1// dear imgui, v1.90.5
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: Multi-Select support
23// [SECTION] Widgets: ListBox
24// [SECTION] Widgets: PlotLines, PlotHistogram
25// [SECTION] Widgets: Value helpers
26// [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc.
27// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
28// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
29// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc.
30
31*/
32
33#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
34#define _CRT_SECURE_NO_WARNINGS
35#endif
36
37#ifndef IMGUI_DEFINE_MATH_OPERATORS
38#define IMGUI_DEFINE_MATH_OPERATORS
39#endif
40
41#include "imgui.h"
42#ifndef IMGUI_DISABLE
43#include "imgui_internal.h"
44
45// System includes
46#include <stdint.h> // intptr_t
47
48//-------------------------------------------------------------------------
49// Warnings
50//-------------------------------------------------------------------------
51
52// Visual Studio warnings
53#ifdef _MSC_VER
54#pragma warning (disable: 4127) // condition expression is constant
55#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen
56#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later
57#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types
58#endif
59#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).
60#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
61#endif
62
63// Clang/GCC warnings with -Weverything
64#if defined(__clang__)
65#if __has_warning("-Wunknown-warning-option")
66#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!
67#endif
68#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx'
69#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
70#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.
71#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.
72#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
73#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
74#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.
75#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
76#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
77#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
78#elif defined(__GNUC__)
79#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
80#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
81#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
82#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
83#endif
84
85//-------------------------------------------------------------------------
86// Data
87//-------------------------------------------------------------------------
88
89// Widgets
90static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
91static 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.
92
93// Those MIN/MAX values are not define because we need to point to them
94static const signed char IM_S8_MIN = -128;
95static const signed char IM_S8_MAX = 127;
96static const unsigned char IM_U8_MIN = 0;
97static const unsigned char IM_U8_MAX = 0xFF;
98static const signed short IM_S16_MIN = -32768;
99static const signed short IM_S16_MAX = 32767;
100static const unsigned short IM_U16_MIN = 0;
101static const unsigned short IM_U16_MAX = 0xFFFF;
102static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
103static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
104static const ImU32 IM_U32_MIN = 0;
105static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
106#ifdef LLONG_MIN
107static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
108static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
109#else
110static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
111static const ImS64 IM_S64_MAX = 9223372036854775807LL;
112#endif
113static const ImU64 IM_U64_MIN = 0;
114#ifdef ULLONG_MAX
115static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
116#else
117static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
118#endif
119
120//-------------------------------------------------------------------------
121// [SECTION] Forward Declarations
122//-------------------------------------------------------------------------
123
124// For InputTextEx()
125static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
126static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
127static 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);
128
129//-------------------------------------------------------------------------
130// [SECTION] Widgets: Text, etc.
131//-------------------------------------------------------------------------
132// - TextEx() [Internal]
133// - TextUnformatted()
134// - Text()
135// - TextV()
136// - TextColored()
137// - TextColoredV()
138// - TextDisabled()
139// - TextDisabledV()
140// - TextWrapped()
141// - TextWrappedV()
142// - LabelText()
143// - LabelTextV()
144// - BulletText()
145// - BulletTextV()
146//-------------------------------------------------------------------------
147
148void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
149{
150 ImGuiWindow* window = GetCurrentWindow();
151 if (window->SkipItems)
152 return;
153 ImGuiContext& g = *GImGui;
154
155 // Accept null ranges
156 if (text == text_end)
157 text = text_end = "";
158
159 // Calculate length
160 const char* text_begin = text;
161 if (text_end == NULL)
162 text_end = text + strlen(s: text); // FIXME-OPT
163
164 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
165 const float wrap_pos_x = window->DC.TextWrapPos;
166 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
167 if (text_end - text <= 2000 || wrap_enabled)
168 {
169 // Common case
170 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(pos: window->DC.CursorPos, wrap_pos_x) : 0.0f;
171 const ImVec2 text_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false, wrap_width);
172
173 ImRect bb(text_pos, text_pos + text_size);
174 ItemSize(size: text_size, text_baseline_y: 0.0f);
175 if (!ItemAdd(bb, id: 0))
176 return;
177
178 // Render (we don't hide text after ## in this end-user function)
179 RenderTextWrapped(pos: bb.Min, text: text_begin, text_end, wrap_width);
180 }
181 else
182 {
183 // Long text!
184 // Perform manual coarse clipping to optimize for long multi-line text
185 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
186 // - 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.
187 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
188 const char* line = text;
189 const float line_height = GetTextLineHeight();
190 ImVec2 text_size(0, 0);
191
192 // Lines to skip (can't skip when logging text)
193 ImVec2 pos = text_pos;
194 if (!g.LogEnabled)
195 {
196 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
197 if (lines_skippable > 0)
198 {
199 int lines_skipped = 0;
200 while (line < text_end && lines_skipped < lines_skippable)
201 {
202 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
203 if (!line_end)
204 line_end = text_end;
205 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
206 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
207 line = line_end + 1;
208 lines_skipped++;
209 }
210 pos.y += lines_skipped * line_height;
211 }
212 }
213
214 // Lines to render
215 if (line < text_end)
216 {
217 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
218 while (line < text_end)
219 {
220 if (IsClippedEx(bb: line_rect, id: 0))
221 break;
222
223 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
224 if (!line_end)
225 line_end = text_end;
226 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
227 RenderText(pos, text: line, text_end: line_end, hide_text_after_hash: false);
228 line = line_end + 1;
229 line_rect.Min.y += line_height;
230 line_rect.Max.y += line_height;
231 pos.y += line_height;
232 }
233
234 // Count remaining lines
235 int lines_skipped = 0;
236 while (line < text_end)
237 {
238 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
239 if (!line_end)
240 line_end = text_end;
241 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
242 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
243 line = line_end + 1;
244 lines_skipped++;
245 }
246 pos.y += lines_skipped * line_height;
247 }
248 text_size.y = (pos - text_pos).y;
249
250 ImRect bb(text_pos, text_pos + text_size);
251 ItemSize(size: text_size, text_baseline_y: 0.0f);
252 ItemAdd(bb, id: 0);
253 }
254}
255
256void ImGui::TextUnformatted(const char* text, const char* text_end)
257{
258 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
259}
260
261void ImGui::Text(const char* fmt, ...)
262{
263 va_list args;
264 va_start(args, fmt);
265 TextV(fmt, args);
266 va_end(args);
267}
268
269void ImGui::TextV(const char* fmt, va_list args)
270{
271 ImGuiWindow* window = GetCurrentWindow();
272 if (window->SkipItems)
273 return;
274
275 const char* text, *text_end;
276 ImFormatStringToTempBufferV(out_buf: &text, out_buf_end: &text_end, fmt, args);
277 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
278}
279
280void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
281{
282 va_list args;
283 va_start(args, fmt);
284 TextColoredV(col, fmt, args);
285 va_end(args);
286}
287
288void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
289{
290 PushStyleColor(idx: ImGuiCol_Text, col);
291 TextV(fmt, args);
292 PopStyleColor();
293}
294
295void ImGui::TextDisabled(const char* fmt, ...)
296{
297 va_list args;
298 va_start(args, fmt);
299 TextDisabledV(fmt, args);
300 va_end(args);
301}
302
303void ImGui::TextDisabledV(const char* fmt, va_list args)
304{
305 ImGuiContext& g = *GImGui;
306 PushStyleColor(idx: ImGuiCol_Text, col: g.Style.Colors[ImGuiCol_TextDisabled]);
307 TextV(fmt, args);
308 PopStyleColor();
309}
310
311void ImGui::TextWrapped(const char* fmt, ...)
312{
313 va_list args;
314 va_start(args, fmt);
315 TextWrappedV(fmt, args);
316 va_end(args);
317}
318
319void ImGui::TextWrappedV(const char* fmt, va_list args)
320{
321 ImGuiContext& g = *GImGui;
322 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
323 if (need_backup)
324 PushTextWrapPos(wrap_local_pos_x: 0.0f);
325 TextV(fmt, args);
326 if (need_backup)
327 PopTextWrapPos();
328}
329
330void ImGui::LabelText(const char* label, const char* fmt, ...)
331{
332 va_list args;
333 va_start(args, fmt);
334 LabelTextV(label, fmt, args);
335 va_end(args);
336}
337
338// Add a label+text combo aligned to other label+value widgets
339void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
340{
341 ImGuiWindow* window = GetCurrentWindow();
342 if (window->SkipItems)
343 return;
344
345 ImGuiContext& g = *GImGui;
346 const ImGuiStyle& style = g.Style;
347 const float w = CalcItemWidth();
348
349 const char* value_text_begin, *value_text_end;
350 ImFormatStringToTempBufferV(out_buf: &value_text_begin, out_buf_end: &value_text_end, fmt, args);
351 const ImVec2 value_size = CalcTextSize(text: value_text_begin, text_end: value_text_end, hide_text_after_double_hash: false);
352 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
353
354 const ImVec2 pos = window->DC.CursorPos;
355 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
356 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));
357 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
358 if (!ItemAdd(bb: total_bb, id: 0))
359 return;
360
361 // Render
362 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));
363 if (label_size.x > 0.0f)
364 RenderText(pos: ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), text: label);
365}
366
367void ImGui::BulletText(const char* fmt, ...)
368{
369 va_list args;
370 va_start(args, fmt);
371 BulletTextV(fmt, args);
372 va_end(args);
373}
374
375// Text with a little bullet aligned to the typical tree node.
376void ImGui::BulletTextV(const char* fmt, va_list args)
377{
378 ImGuiWindow* window = GetCurrentWindow();
379 if (window->SkipItems)
380 return;
381
382 ImGuiContext& g = *GImGui;
383 const ImGuiStyle& style = g.Style;
384
385 const char* text_begin, *text_end;
386 ImFormatStringToTempBufferV(out_buf: &text_begin, out_buf_end: &text_end, fmt, args);
387 const ImVec2 label_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false);
388 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
389 ImVec2 pos = window->DC.CursorPos;
390 pos.y += window->DC.CurrLineTextBaseOffset;
391 ItemSize(size: total_size, text_baseline_y: 0.0f);
392 const ImRect bb(pos, pos + total_size);
393 if (!ItemAdd(bb, id: 0))
394 return;
395
396 // Render
397 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
398 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), col: text_col);
399 RenderText(pos: bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text: text_begin, text_end, hide_text_after_hash: false);
400}
401
402//-------------------------------------------------------------------------
403// [SECTION] Widgets: Main
404//-------------------------------------------------------------------------
405// - ButtonBehavior() [Internal]
406// - Button()
407// - SmallButton()
408// - InvisibleButton()
409// - ArrowButton()
410// - CloseButton() [Internal]
411// - CollapseButton() [Internal]
412// - GetWindowScrollbarID() [Internal]
413// - GetWindowScrollbarRect() [Internal]
414// - Scrollbar() [Internal]
415// - ScrollbarEx() [Internal]
416// - Image()
417// - ImageButton()
418// - Checkbox()
419// - CheckboxFlagsT() [Internal]
420// - CheckboxFlags()
421// - RadioButton()
422// - ProgressBar()
423// - Bullet()
424//-------------------------------------------------------------------------
425
426// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
427// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
428// this code is a little complex.
429// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
430// See the series of events below and the corresponding state reported by dear imgui:
431//------------------------------------------------------------------------------------------------------------------------------------------------
432// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
433// Frame N+0 (mouse is outside bb) - - - - - -
434// Frame N+1 (mouse moves inside bb) - true - - - -
435// Frame N+2 (mouse button is down) - true true true - true
436// Frame N+3 (mouse button is down) - true true - - -
437// Frame N+4 (mouse moves outside bb) - - true - - -
438// Frame N+5 (mouse moves inside bb) - true true - - -
439// Frame N+6 (mouse button is released) true true - - true -
440// Frame N+7 (mouse button is released) - true - - - -
441// Frame N+8 (mouse moves outside bb) - - - - - -
442//------------------------------------------------------------------------------------------------------------------------------------------------
443// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
444// Frame N+2 (mouse button is down) true true true true - true
445// Frame N+3 (mouse button is down) - true true - - -
446// Frame N+6 (mouse button is released) - true - - true -
447// Frame N+7 (mouse button is released) - true - - - -
448//------------------------------------------------------------------------------------------------------------------------------------------------
449// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
450// Frame N+2 (mouse button is down) - true - - - true
451// Frame N+3 (mouse button is down) - true - - - -
452// Frame N+6 (mouse button is released) true true - - - -
453// Frame N+7 (mouse button is released) - true - - - -
454//------------------------------------------------------------------------------------------------------------------------------------------------
455// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
456// Frame N+0 (mouse button is down) - true - - - true
457// Frame N+1 (mouse button is down) - true - - - -
458// Frame N+2 (mouse button is released) - true - - - -
459// Frame N+3 (mouse button is released) - true - - - -
460// Frame N+4 (mouse button is down) true true true true - true
461// Frame N+5 (mouse button is down) - true true - - -
462// Frame N+6 (mouse button is released) - true - - true -
463// Frame N+7 (mouse button is released) - true - - - -
464//------------------------------------------------------------------------------------------------------------------------------------------------
465// Note that some combinations are supported,
466// - PressedOnDragDropHold can generally be associated with any flag.
467// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
468//------------------------------------------------------------------------------------------------------------------------------------------------
469// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set:
470// Repeat+ Repeat+ Repeat+ Repeat+
471// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
472//-------------------------------------------------------------------------------------------------------------------------------------------------
473// Frame N+0 (mouse button is down) - true - true
474// ... - - - -
475// Frame N + RepeatDelay true true - true
476// ... - - - -
477// Frame N + RepeatDelay + RepeatRate*N true true - true
478//-------------------------------------------------------------------------------------------------------------------------------------------------
479
480// FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
481// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
482// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
483bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
484{
485 ImGuiContext& g = *GImGui;
486 ImGuiWindow* window = GetCurrentWindow();
487
488 // Default only reacts to left mouse button
489 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
490 flags |= ImGuiButtonFlags_MouseButtonDefault_;
491
492 // Default behavior requires click + release inside bounding box
493 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
494 flags |= ImGuiButtonFlags_PressedOnDefault_;
495
496 // Default behavior inherited from item flags
497 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
498 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.InFlags : g.CurrentItemFlags);
499 if (flags & ImGuiButtonFlags_AllowOverlap)
500 item_flags |= ImGuiItemFlags_AllowOverlap;
501 if (flags & ImGuiButtonFlags_Repeat)
502 item_flags |= ImGuiItemFlags_ButtonRepeat;
503
504 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
505 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
506 if (flatten_hovered_children)
507 g.HoveredWindow = window;
508
509#ifdef IMGUI_ENABLE_TEST_ENGINE
510 // Alternate registration spot, for when caller didn't use ItemAdd()
511 if (id != 0 && g.LastItemData.ID != id)
512 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
513#endif
514
515 bool pressed = false;
516 bool hovered = ItemHoverable(bb, id, item_flags);
517
518 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
519 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
520 if (IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
521 {
522 hovered = true;
523 SetHoveredID(id);
524 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
525 {
526 pressed = true;
527 g.DragDropHoldJustPressedId = id;
528 FocusWindow(window);
529 }
530 }
531
532 if (flatten_hovered_children)
533 g.HoveredWindow = backup_hovered_window;
534
535 // Mouse handling
536 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
537 if (hovered)
538 {
539 // Poll mouse buttons
540 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
541 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
542 int mouse_button_clicked = -1;
543 int mouse_button_released = -1;
544 for (int button = 0; button < 3; button++)
545 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
546 {
547 if (IsMouseClicked(button, owner_id: test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
548 if (IsMouseReleased(button, owner_id: test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
549 }
550
551 // Process initial action
552 if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt))
553 {
554 if (mouse_button_clicked != -1 && g.ActiveId != id)
555 {
556 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
557 SetKeyOwner(key: MouseButtonToKey(button: mouse_button_clicked), owner_id: id);
558 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
559 {
560 SetActiveID(id, window);
561 g.ActiveIdMouseButton = mouse_button_clicked;
562 if (!(flags & ImGuiButtonFlags_NoNavFocus))
563 SetFocusID(id, window);
564 FocusWindow(window);
565 }
566 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
567 {
568 pressed = true;
569 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
570 ClearActiveID();
571 else
572 SetActiveID(id, window); // Hold on ID
573 if (!(flags & ImGuiButtonFlags_NoNavFocus))
574 SetFocusID(id, window);
575 g.ActiveIdMouseButton = mouse_button_clicked;
576 FocusWindow(window);
577 }
578 }
579 if (flags & ImGuiButtonFlags_PressedOnRelease)
580 {
581 if (mouse_button_released != -1)
582 {
583 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
584 if (!has_repeated_at_least_once)
585 pressed = true;
586 if (!(flags & ImGuiButtonFlags_NoNavFocus))
587 SetFocusID(id, window);
588 ClearActiveID();
589 }
590 }
591
592 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
593 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
594 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
595 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(button: g.ActiveIdMouseButton, owner_id: test_owner_id, flags: ImGuiInputFlags_Repeat))
596 pressed = true;
597 }
598
599 if (pressed)
600 g.NavDisableHighlight = true;
601 }
602
603 // Gamepad/Keyboard handling
604 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
605 if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover)
606 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
607 hovered = true;
608 if (g.NavActivateDownId == id)
609 {
610 bool nav_activated_by_code = (g.NavActivateId == id);
611 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
612 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
613 {
614 // Avoid pressing multiple keys from triggering excessive amount of repeat events
615 const ImGuiKeyData* key1 = GetKeyData(key: ImGuiKey_Space);
616 const ImGuiKeyData* key2 = GetKeyData(key: ImGuiKey_Enter);
617 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
618 const float t1 = ImMax(lhs: ImMax(lhs: key1->DownDuration, rhs: key2->DownDuration), rhs: key3->DownDuration);
619 nav_activated_by_inputs = CalcTypematicRepeatAmount(t0: t1 - g.IO.DeltaTime, t1, repeat_delay: g.IO.KeyRepeatDelay, repeat_rate: g.IO.KeyRepeatRate) > 0;
620 }
621 if (nav_activated_by_code || nav_activated_by_inputs)
622 {
623 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
624 pressed = true;
625 SetActiveID(id, window);
626 g.ActiveIdSource = g.NavInputSource;
627 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
628 SetFocusID(id, window);
629 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
630 g.ActiveIdFromShortcut = true;
631 }
632 }
633
634 // Process while held
635 bool held = false;
636 if (g.ActiveId == id)
637 {
638 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
639 {
640 if (g.ActiveIdIsJustActivated)
641 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
642
643 const int mouse_button = g.ActiveIdMouseButton;
644 if (mouse_button == -1)
645 {
646 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
647 ClearActiveID();
648 }
649 else if (IsMouseDown(button: mouse_button, owner_id: test_owner_id))
650 {
651 held = true;
652 }
653 else
654 {
655 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
656 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
657 if ((release_in || release_anywhere) && !g.DragDropActive)
658 {
659 // Report as pressed when releasing the mouse (this is the most common path)
660 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
661 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
662 bool is_button_avail_or_owned = TestKeyOwner(key: MouseButtonToKey(button: mouse_button), owner_id: test_owner_id);
663 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
664 pressed = true;
665 }
666 ClearActiveID();
667 }
668 if (!(flags & ImGuiButtonFlags_NoNavFocus))
669 g.NavDisableHighlight = true;
670 }
671 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
672 {
673 // When activated using Nav, we hold on the ActiveID until activation button is released
674 if (g.NavActivateDownId == id)
675 held = true; // hovered == true not true as we are already likely hovered on direct activation.
676 else
677 ClearActiveID();
678 }
679 if (pressed)
680 g.ActiveIdHasBeenPressedBefore = true;
681 }
682
683 // Activation highlight (this may be a remote activation)
684 if (g.NavHighlightActivatedId == id)
685 hovered = true;
686
687 if (out_hovered) *out_hovered = hovered;
688 if (out_held) *out_held = held;
689
690 return pressed;
691}
692
693bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
694{
695 ImGuiWindow* window = GetCurrentWindow();
696 if (window->SkipItems)
697 return false;
698
699 ImGuiContext& g = *GImGui;
700 const ImGuiStyle& style = g.Style;
701 const ImGuiID id = window->GetID(str: label);
702 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
703
704 ImVec2 pos = window->DC.CursorPos;
705 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)
706 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
707 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);
708
709 const ImRect bb(pos, pos + size);
710 ItemSize(size, text_baseline_y: style.FramePadding.y);
711 if (!ItemAdd(bb, id))
712 return false;
713
714 bool hovered, held;
715 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
716
717 // Render
718 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
719 RenderNavHighlight(bb, id);
720 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: true, rounding: style.FrameRounding);
721
722 if (g.LogEnabled)
723 LogSetNextTextDecoration(prefix: "[", suffix: "]");
724 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);
725
726 // Automatically close popups
727 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
728 // CloseCurrentPopup();
729
730 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
731 return pressed;
732}
733
734bool ImGui::Button(const char* label, const ImVec2& size_arg)
735{
736 return ButtonEx(label, size_arg, flags: ImGuiButtonFlags_None);
737}
738
739// Small buttons fits within text without additional vertical spacing.
740bool ImGui::SmallButton(const char* label)
741{
742 ImGuiContext& g = *GImGui;
743 float backup_padding_y = g.Style.FramePadding.y;
744 g.Style.FramePadding.y = 0.0f;
745 bool pressed = ButtonEx(label, size_arg: ImVec2(0, 0), flags: ImGuiButtonFlags_AlignTextBaseLine);
746 g.Style.FramePadding.y = backup_padding_y;
747 return pressed;
748}
749
750// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
751// 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)
752bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
753{
754 ImGuiContext& g = *GImGui;
755 ImGuiWindow* window = GetCurrentWindow();
756 if (window->SkipItems)
757 return false;
758
759 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
760 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
761
762 const ImGuiID id = window->GetID(str: str_id);
763 ImVec2 size = CalcItemSize(size: size_arg, default_w: 0.0f, default_h: 0.0f);
764 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
765 ItemSize(size);
766 if (!ItemAdd(bb, id))
767 return false;
768
769 bool hovered, held;
770 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
771
772 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
773 return pressed;
774}
775
776bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
777{
778 ImGuiContext& g = *GImGui;
779 ImGuiWindow* window = GetCurrentWindow();
780 if (window->SkipItems)
781 return false;
782
783 const ImGuiID id = window->GetID(str: str_id);
784 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
785 const float default_size = GetFrameHeight();
786 ItemSize(size, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
787 if (!ItemAdd(bb, id))
788 return false;
789
790 bool hovered, held;
791 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
792
793 // Render
794 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
795 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
796 RenderNavHighlight(bb, id);
797 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: bg_col, border: true, rounding: g.Style.FrameRounding);
798 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);
799
800 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
801 return pressed;
802}
803
804bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
805{
806 float sz = GetFrameHeight();
807 return ArrowButtonEx(str_id, dir, size: ImVec2(sz, sz), flags: ImGuiButtonFlags_None);
808}
809
810// Button to close a window
811bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
812{
813 ImGuiContext& g = *GImGui;
814 ImGuiWindow* window = g.CurrentWindow;
815
816 // 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)
817 // 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?
818 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
819 ImRect bb_interact = bb;
820 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
821 if (area_to_visible_ratio < 1.5f)
822 bb_interact.Expand(amount: ImTrunc(v: bb_interact.GetSize() * -0.25f));
823
824 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
825 // (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).
826 bool is_clipped = !ItemAdd(bb: bb_interact, id);
827
828 bool hovered, held;
829 bool pressed = ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held);
830 if (is_clipped)
831 return pressed;
832
833 // Render
834 // FIXME: Clarify this mess
835 ImU32 col = GetColorU32(idx: held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
836 ImVec2 center = bb.GetCenter();
837 if (hovered)
838 window->DrawList->AddCircleFilled(center, radius: ImMax(lhs: 2.0f, rhs: g.FontSize * 0.5f + 1.0f), col);
839
840 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
841 ImU32 cross_col = GetColorU32(idx: ImGuiCol_Text);
842 center -= ImVec2(0.5f, 0.5f);
843 window->DrawList->AddLine(p1: center + ImVec2(+cross_extent, +cross_extent), p2: center + ImVec2(-cross_extent, -cross_extent), col: cross_col, thickness: 1.0f);
844 window->DrawList->AddLine(p1: center + ImVec2(+cross_extent, -cross_extent), p2: center + ImVec2(-cross_extent, +cross_extent), col: cross_col, thickness: 1.0f);
845
846 return pressed;
847}
848
849// The Collapse button also functions as a Dock Menu button.
850bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
851{
852 ImGuiContext& g = *GImGui;
853 ImGuiWindow* window = g.CurrentWindow;
854
855 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
856 bool is_clipped = !ItemAdd(bb, id);
857 bool hovered, held;
858 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_None);
859 if (is_clipped)
860 return pressed;
861
862 // Render
863 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
864 ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
865 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
866 if (hovered || held)
867 window->DrawList->AddCircleFilled(center: bb.GetCenter() + ImVec2(0.0f, -0.5f), radius: g.FontSize * 0.5f + 1.0f, col: bg_col);
868
869 if (dock_node)
870 RenderArrowDockMenu(draw_list: window->DrawList, p_min: bb.Min, sz: g.FontSize, col: text_col);
871 else
872 RenderArrow(draw_list: window->DrawList, pos: bb.Min, col: text_col, dir: window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, scale: 1.0f);
873
874 // Switch to moving the window after mouse is moved beyond the initial drag threshold
875 if (IsItemActive() && IsMouseDragging(button: 0))
876 StartMouseMovingWindowOrNode(window, node: dock_node, undock: true); // Undock from window/collapse menu button
877
878 return pressed;
879}
880
881ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
882{
883 return window->GetID(str: axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
884}
885
886// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
887ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
888{
889 const ImRect outer_rect = window->Rect();
890 const ImRect inner_rect = window->InnerRect;
891 const float border_size = window->WindowBorderSize;
892 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
893 IM_ASSERT(scrollbar_size > 0.0f);
894 if (axis == ImGuiAxis_X)
895 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);
896 else
897 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);
898}
899
900void ImGui::Scrollbar(ImGuiAxis axis)
901{
902 ImGuiContext& g = *GImGui;
903 ImGuiWindow* window = g.CurrentWindow;
904 const ImGuiID id = GetWindowScrollbarID(window, axis);
905
906 // Calculate scrollbar bounding box
907 ImRect bb = GetWindowScrollbarRect(window, axis);
908 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
909 if (axis == ImGuiAxis_X)
910 {
911 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
912 if (!window->ScrollbarY)
913 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
914 }
915 else
916 {
917 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
918 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
919 if (!window->ScrollbarX)
920 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
921 }
922 float size_avail = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
923 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
924 ImS64 scroll = (ImS64)window->Scroll[axis];
925 ScrollbarEx(bb, id, axis, p_scroll_v: &scroll, avail_v: (ImS64)size_avail, contents_v: (ImS64)size_contents, flags: rounding_corners);
926 window->Scroll[axis] = (float)scroll;
927}
928
929// Vertical/Horizontal scrollbar
930// The entire piece of code below is rather confusing because:
931// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
932// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
933// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
934// Still, the code should probably be made simpler..
935bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_avail_v, ImS64 size_contents_v, ImDrawFlags flags)
936{
937 ImGuiContext& g = *GImGui;
938 ImGuiWindow* window = g.CurrentWindow;
939 if (window->SkipItems)
940 return false;
941
942 const float bb_frame_width = bb_frame.GetWidth();
943 const float bb_frame_height = bb_frame.GetHeight();
944 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
945 return false;
946
947 // 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)
948 float alpha = 1.0f;
949 if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
950 alpha = ImSaturate(f: (bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
951 if (alpha <= 0.0f)
952 return false;
953
954 const ImGuiStyle& style = g.Style;
955 const bool allow_interaction = (alpha >= 1.0f);
956
957 ImRect bb = bb_frame;
958 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)));
959
960 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
961 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
962
963 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
964 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
965 IM_ASSERT(ImMax(size_contents_v, size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers.
966 const ImS64 win_size_v = ImMax(lhs: ImMax(lhs: size_contents_v, rhs: size_avail_v), rhs: (ImS64)1);
967 const float grab_h_pixels = ImClamp(v: scrollbar_size_v * ((float)size_avail_v / (float)win_size_v), mn: style.GrabMinSize, mx: scrollbar_size_v);
968 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
969
970 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
971 bool held = false;
972 bool hovered = false;
973 ItemAdd(bb: bb_frame, id, NULL, extra_flags: ImGuiItemFlags_NoNav);
974 ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_NoNavFocus);
975
976 const ImS64 scroll_max = ImMax(lhs: (ImS64)1, rhs: size_contents_v - size_avail_v);
977 float scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
978 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
979 if (held && allow_interaction && grab_h_norm < 1.0f)
980 {
981 const float scrollbar_pos_v = bb.Min[axis];
982 const float mouse_pos_v = g.IO.MousePos[axis];
983
984 // Click position in scrollbar normalized space (0.0f->1.0f)
985 const float clicked_v_norm = ImSaturate(f: (mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
986
987 bool seek_absolute = false;
988 if (g.ActiveIdIsJustActivated)
989 {
990 // On initial click calculate the distance between mouse and the center of the grab
991 seek_absolute = (clicked_v_norm < grab_v_norm || clicked_v_norm > grab_v_norm + grab_h_norm);
992 if (seek_absolute)
993 g.ScrollbarClickDeltaToGrabCenter = 0.0f;
994 else
995 g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
996 }
997
998 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
999 // 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
1000 const float scroll_v_norm = ImSaturate(f: (clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1001 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1002
1003 // Update values for rendering
1004 scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1005 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1006
1007 // Update distance to grab now that we have seeked and saturated
1008 if (seek_absolute)
1009 g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1010 }
1011
1012 // Render
1013 const ImU32 bg_col = GetColorU32(idx: ImGuiCol_ScrollbarBg);
1014 const ImU32 grab_col = GetColorU32(idx: held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha_mul: alpha);
1015 window->DrawList->AddRectFilled(p_min: bb_frame.Min, p_max: bb_frame.Max, col: bg_col, rounding: window->WindowRounding, flags);
1016 ImRect grab_rect;
1017 if (axis == ImGuiAxis_X)
1018 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);
1019 else
1020 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);
1021 window->DrawList->AddRectFilled(p_min: grab_rect.Min, p_max: grab_rect.Max, col: grab_col, rounding: style.ScrollbarRounding);
1022
1023 return held;
1024}
1025
1026// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1027// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1028void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1029{
1030 ImGuiWindow* window = GetCurrentWindow();
1031 if (window->SkipItems)
1032 return;
1033
1034 const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f;
1035 const ImVec2 padding(border_size, border_size);
1036 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1037 ItemSize(bb);
1038 if (!ItemAdd(bb, id: 0))
1039 return;
1040
1041 // Render
1042 if (border_size > 0.0f)
1043 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);
1044 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));
1045}
1046
1047// ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390)
1048// We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API.
1049bool 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)
1050{
1051 ImGuiContext& g = *GImGui;
1052 ImGuiWindow* window = GetCurrentWindow();
1053 if (window->SkipItems)
1054 return false;
1055
1056 const ImVec2 padding = g.Style.FramePadding;
1057 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1058 ItemSize(bb);
1059 if (!ItemAdd(bb, id))
1060 return false;
1061
1062 bool hovered, held;
1063 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
1064
1065 // Render
1066 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1067 RenderNavHighlight(bb, id);
1068 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));
1069 if (bg_col.w > 0.0f)
1070 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1071 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));
1072
1073 return pressed;
1074}
1075
1076// Note that ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1077bool 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)
1078{
1079 ImGuiContext& g = *GImGui;
1080 ImGuiWindow* window = g.CurrentWindow;
1081 if (window->SkipItems)
1082 return false;
1083
1084 return ImageButtonEx(id: window->GetID(str: str_id), texture_id: user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
1085}
1086
1087#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1088// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1089// - 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)
1090// - new ImageButton() always use style.FramePadding Old ImageButton() had an override argument.
1091// If you need to change padding with new ImageButton() you can use PushStyleVar(ImGuiStyleVar_FramePadding, value), consistent with other Button functions.
1092bool 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)
1093{
1094 ImGuiContext& g = *GImGui;
1095 ImGuiWindow* window = g.CurrentWindow;
1096 if (window->SkipItems)
1097 return false;
1098
1099 // Default to using texture ID as ID. User can still push string/integer prefixes.
1100 PushID(ptr_id: (void*)(intptr_t)user_texture_id);
1101 const ImGuiID id = window->GetID(str: "#image");
1102 PopID();
1103
1104 if (frame_padding >= 0)
1105 PushStyleVar(idx: ImGuiStyleVar_FramePadding, val: ImVec2((float)frame_padding, (float)frame_padding));
1106 bool ret = ImageButtonEx(id, texture_id: user_texture_id, image_size: size, uv0, uv1, bg_col, tint_col);
1107 if (frame_padding >= 0)
1108 PopStyleVar();
1109 return ret;
1110}
1111#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1112
1113bool ImGui::Checkbox(const char* label, bool* v)
1114{
1115 ImGuiWindow* window = GetCurrentWindow();
1116 if (window->SkipItems)
1117 return false;
1118
1119 ImGuiContext& g = *GImGui;
1120 const ImGuiStyle& style = g.Style;
1121 const ImGuiID id = window->GetID(str: label);
1122 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1123
1124 const float square_sz = GetFrameHeight();
1125 const ImVec2 pos = window->DC.CursorPos;
1126 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));
1127 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1128 if (!ItemAdd(bb: total_bb, id))
1129 {
1130 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1131 return false;
1132 }
1133
1134 bool hovered, held;
1135 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1136 if (pressed)
1137 {
1138 *v = !(*v);
1139 MarkItemEdited(id);
1140 }
1141
1142 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1143 RenderNavHighlight(bb: total_bb, id);
1144 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);
1145 ImU32 check_col = GetColorU32(idx: ImGuiCol_CheckMark);
1146 bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0;
1147 if (mixed_value)
1148 {
1149 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1150 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1151 ImVec2 pad(ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 3.6f)));
1152 window->DrawList->AddRectFilled(p_min: check_bb.Min + pad, p_max: check_bb.Max - pad, col: check_col, rounding: style.FrameRounding);
1153 }
1154 else if (*v)
1155 {
1156 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1157 RenderCheckMark(draw_list: window->DrawList, pos: check_bb.Min + ImVec2(pad, pad), col: check_col, sz: square_sz - pad * 2.0f);
1158 }
1159
1160 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1161 if (g.LogEnabled)
1162 LogRenderedText(ref_pos: &label_pos, text: mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1163 if (label_size.x > 0.0f)
1164 RenderText(pos: label_pos, text: label);
1165
1166 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1167 return pressed;
1168}
1169
1170template<typename T>
1171bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1172{
1173 bool all_on = (*flags & flags_value) == flags_value;
1174 bool any_on = (*flags & flags_value) != 0;
1175 bool pressed;
1176 if (!all_on && any_on)
1177 {
1178 ImGuiContext& g = *GImGui;
1179 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1180 pressed = Checkbox(label, v: &all_on);
1181 }
1182 else
1183 {
1184 pressed = Checkbox(label, v: &all_on);
1185
1186 }
1187 if (pressed)
1188 {
1189 if (all_on)
1190 *flags |= flags_value;
1191 else
1192 *flags &= ~flags_value;
1193 }
1194 return pressed;
1195}
1196
1197bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1198{
1199 return CheckboxFlagsT(label, flags, flags_value);
1200}
1201
1202bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1203{
1204 return CheckboxFlagsT(label, flags, flags_value);
1205}
1206
1207bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1208{
1209 return CheckboxFlagsT(label, flags, flags_value);
1210}
1211
1212bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1213{
1214 return CheckboxFlagsT(label, flags, flags_value);
1215}
1216
1217bool ImGui::RadioButton(const char* label, bool active)
1218{
1219 ImGuiWindow* window = GetCurrentWindow();
1220 if (window->SkipItems)
1221 return false;
1222
1223 ImGuiContext& g = *GImGui;
1224 const ImGuiStyle& style = g.Style;
1225 const ImGuiID id = window->GetID(str: label);
1226 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1227
1228 const float square_sz = GetFrameHeight();
1229 const ImVec2 pos = window->DC.CursorPos;
1230 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1231 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));
1232 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1233 if (!ItemAdd(bb: total_bb, id))
1234 return false;
1235
1236 ImVec2 center = check_bb.GetCenter();
1237 center.x = IM_ROUND(center.x);
1238 center.y = IM_ROUND(center.y);
1239 const float radius = (square_sz - 1.0f) * 0.5f;
1240
1241 bool hovered, held;
1242 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1243 if (pressed)
1244 MarkItemEdited(id);
1245
1246 RenderNavHighlight(bb: total_bb, id);
1247 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1248 window->DrawList->AddCircleFilled(center, radius, col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segments: num_segment);
1249 if (active)
1250 {
1251 const float pad = ImMax(lhs: 1.0f, IM_TRUNC(square_sz / 6.0f));
1252 window->DrawList->AddCircleFilled(center, radius: radius - pad, col: GetColorU32(idx: ImGuiCol_CheckMark));
1253 }
1254
1255 if (style.FrameBorderSize > 0.0f)
1256 {
1257 window->DrawList->AddCircle(center: center + ImVec2(1, 1), radius, col: GetColorU32(idx: ImGuiCol_BorderShadow), num_segments: num_segment, thickness: style.FrameBorderSize);
1258 window->DrawList->AddCircle(center, radius, col: GetColorU32(idx: ImGuiCol_Border), num_segments: num_segment, thickness: style.FrameBorderSize);
1259 }
1260
1261 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1262 if (g.LogEnabled)
1263 LogRenderedText(ref_pos: &label_pos, text: active ? "(x)" : "( )");
1264 if (label_size.x > 0.0f)
1265 RenderText(pos: label_pos, text: label);
1266
1267 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1268 return pressed;
1269}
1270
1271// 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..
1272bool ImGui::RadioButton(const char* label, int* v, int v_button)
1273{
1274 const bool pressed = RadioButton(label, active: *v == v_button);
1275 if (pressed)
1276 *v = v_button;
1277 return pressed;
1278}
1279
1280// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1281void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1282{
1283 ImGuiWindow* window = GetCurrentWindow();
1284 if (window->SkipItems)
1285 return;
1286
1287 ImGuiContext& g = *GImGui;
1288 const ImGuiStyle& style = g.Style;
1289
1290 ImVec2 pos = window->DC.CursorPos;
1291 ImVec2 size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: g.FontSize + style.FramePadding.y * 2.0f);
1292 ImRect bb(pos, pos + size);
1293 ItemSize(size, text_baseline_y: style.FramePadding.y);
1294 if (!ItemAdd(bb, id: 0))
1295 return;
1296
1297 // Out of courtesy we accept a NaN fraction without crashing
1298 fraction = ImSaturate(f: fraction);
1299 const float fraction_not_nan = (fraction == fraction) ? fraction : 0.0f;
1300
1301 // Render
1302 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
1303 bb.Expand(amount: ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1304 const ImVec2 fill_br = ImVec2(ImLerp(a: bb.Min.x, b: bb.Max.x, t: fraction_not_nan), bb.Max.y);
1305 RenderRectFilledRangeH(draw_list: window->DrawList, rect: bb, col: GetColorU32(idx: ImGuiCol_PlotHistogram), x_start_norm: 0.0f, x_end_norm: fraction_not_nan, rounding: style.FrameRounding);
1306
1307 // Default displaying the fraction as percentage string, but user can override it
1308 char overlay_buf[32];
1309 if (!overlay)
1310 {
1311 ImFormatString(buf: overlay_buf, IM_ARRAYSIZE(overlay_buf), fmt: "%.0f%%", fraction * 100 + 0.01f);
1312 overlay = overlay_buf;
1313 }
1314
1315 ImVec2 overlay_size = CalcTextSize(text: overlay, NULL);
1316 if (overlay_size.x > 0.0f)
1317 RenderTextClipped(pos_min: ImVec2(ImClamp(v: fill_br.x + style.ItemSpacing.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);
1318}
1319
1320void ImGui::Bullet()
1321{
1322 ImGuiWindow* window = GetCurrentWindow();
1323 if (window->SkipItems)
1324 return;
1325
1326 ImGuiContext& g = *GImGui;
1327 const ImGuiStyle& style = g.Style;
1328 const float line_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: g.FontSize);
1329 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1330 ItemSize(bb);
1331 if (!ItemAdd(bb, id: 0))
1332 {
1333 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2);
1334 return;
1335 }
1336
1337 // Render and stay on same line
1338 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1339 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), col: text_col);
1340 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2.0f);
1341}
1342
1343//-------------------------------------------------------------------------
1344// [SECTION] Widgets: Low-level Layout helpers
1345//-------------------------------------------------------------------------
1346// - Spacing()
1347// - Dummy()
1348// - NewLine()
1349// - AlignTextToFramePadding()
1350// - SeparatorEx() [Internal]
1351// - Separator()
1352// - SplitterBehavior() [Internal]
1353// - ShrinkWidths() [Internal]
1354//-------------------------------------------------------------------------
1355
1356void ImGui::Spacing()
1357{
1358 ImGuiWindow* window = GetCurrentWindow();
1359 if (window->SkipItems)
1360 return;
1361 ItemSize(size: ImVec2(0, 0));
1362}
1363
1364void ImGui::Dummy(const ImVec2& size)
1365{
1366 ImGuiWindow* window = GetCurrentWindow();
1367 if (window->SkipItems)
1368 return;
1369
1370 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1371 ItemSize(size);
1372 ItemAdd(bb, id: 0);
1373}
1374
1375void ImGui::NewLine()
1376{
1377 ImGuiWindow* window = GetCurrentWindow();
1378 if (window->SkipItems)
1379 return;
1380
1381 ImGuiContext& g = *GImGui;
1382 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1383 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1384 window->DC.IsSameLine = false;
1385 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.
1386 ItemSize(size: ImVec2(0, 0));
1387 else
1388 ItemSize(size: ImVec2(0.0f, g.FontSize));
1389 window->DC.LayoutType = backup_layout_type;
1390}
1391
1392void ImGui::AlignTextToFramePadding()
1393{
1394 ImGuiWindow* window = GetCurrentWindow();
1395 if (window->SkipItems)
1396 return;
1397
1398 ImGuiContext& g = *GImGui;
1399 window->DC.CurrLineSize.y = ImMax(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + g.Style.FramePadding.y * 2);
1400 window->DC.CurrLineTextBaseOffset = ImMax(lhs: window->DC.CurrLineTextBaseOffset, rhs: g.Style.FramePadding.y);
1401}
1402
1403// Horizontal/vertical separating line
1404// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1405// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1406void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1407{
1408 ImGuiWindow* window = GetCurrentWindow();
1409 if (window->SkipItems)
1410 return;
1411
1412 ImGuiContext& g = *GImGui;
1413 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1414 IM_ASSERT(thickness > 0.0f);
1415
1416 if (flags & ImGuiSeparatorFlags_Vertical)
1417 {
1418 // Vertical separator, for menu bars (use current line height).
1419 float y1 = window->DC.CursorPos.y;
1420 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1421 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1422 ItemSize(size: ImVec2(thickness, 0.0f));
1423 if (!ItemAdd(bb, id: 0))
1424 return;
1425
1426 // Draw
1427 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1428 if (g.LogEnabled)
1429 LogText(fmt: " |");
1430 }
1431 else if (flags & ImGuiSeparatorFlags_Horizontal)
1432 {
1433 // Horizontal Separator
1434 float x1 = window->DC.CursorPos.x;
1435 float x2 = window->WorkRect.Max.x;
1436
1437 // Preserve legacy behavior inside Columns()
1438 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1439 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1440 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1441 if (columns)
1442 {
1443 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1444 x2 = window->Pos.x + window->Size.x;
1445 PushColumnsBackground();
1446 }
1447
1448 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1449 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1450 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.
1451 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1452 ItemSize(size: ImVec2(0.0f, thickness_for_layout));
1453
1454 if (ItemAdd(bb, id: 0))
1455 {
1456 // Draw
1457 window->DrawList->AddRectFilled(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(idx: ImGuiCol_Separator));
1458 if (g.LogEnabled)
1459 LogRenderedText(ref_pos: &bb.Min, text: "--------------------------------\n");
1460
1461 }
1462 if (columns)
1463 {
1464 PopColumnsBackground();
1465 columns->LineMinY = window->DC.CursorPos.y;
1466 }
1467 }
1468}
1469
1470void ImGui::Separator()
1471{
1472 ImGuiContext& g = *GImGui;
1473 ImGuiWindow* window = g.CurrentWindow;
1474 if (window->SkipItems)
1475 return;
1476
1477 // Those flags should eventually be configurable by the user
1478 // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1479 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1480
1481 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1482 if (window->DC.CurrentColumns)
1483 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1484
1485 SeparatorEx(flags, thickness: 1.0f);
1486}
1487
1488void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1489{
1490 ImGuiContext& g = *GImGui;
1491 ImGuiWindow* window = g.CurrentWindow;
1492 ImGuiStyle& style = g.Style;
1493
1494 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
1495 const ImVec2 pos = window->DC.CursorPos;
1496 const ImVec2 padding = style.SeparatorTextPadding;
1497
1498 const float separator_thickness = style.SeparatorTextBorderSize;
1499 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));
1500 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1501 const float text_baseline_y = ImTrunc(f: (bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1502 ItemSize(size: min_size, text_baseline_y);
1503 if (!ItemAdd(bb, id))
1504 return;
1505
1506 const float sep1_x1 = pos.x;
1507 const float sep2_x2 = bb.Max.x;
1508 const float seps_y = ImTrunc(f: (bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1509
1510 const float label_avail_w = ImMax(lhs: 0.0f, rhs: sep2_x2 - sep1_x1 - padding.x * 2.0f);
1511 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
1512
1513 // This allows using SameLine() to position something in the 'extra_w'
1514 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1515
1516 const ImU32 separator_col = GetColorU32(idx: ImGuiCol_Separator);
1517 if (label_size.x > 0.0f)
1518 {
1519 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1520 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1521 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1522 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep1_x2, seps_y), col: separator_col, thickness: separator_thickness);
1523 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1524 window->DrawList->AddLine(p1: ImVec2(sep2_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1525 if (g.LogEnabled)
1526 LogSetNextTextDecoration(prefix: "---", NULL);
1527 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);
1528 }
1529 else
1530 {
1531 if (g.LogEnabled)
1532 LogText(fmt: "---");
1533 if (separator_thickness > 0.0f)
1534 window->DrawList->AddLine(p1: ImVec2(sep1_x1, seps_y), p2: ImVec2(sep2_x2, seps_y), col: separator_col, thickness: separator_thickness);
1535 }
1536}
1537
1538void ImGui::SeparatorText(const char* label)
1539{
1540 ImGuiWindow* window = GetCurrentWindow();
1541 if (window->SkipItems)
1542 return;
1543
1544 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1545 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1546 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1547 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1548 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1549 // and then we can turn this into a format function.
1550 SeparatorTextEx(id: 0, label, label_end: FindRenderedTextEnd(text: label), extra_w: 0.0f);
1551}
1552
1553// 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.
1554bool 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)
1555{
1556 ImGuiContext& g = *GImGui;
1557 ImGuiWindow* window = g.CurrentWindow;
1558
1559 if (!ItemAdd(bb, id, NULL, extra_flags: ImGuiItemFlags_NoNav))
1560 return false;
1561
1562 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1563 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1564 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1565 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1566#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1567 button_flags |= ImGuiButtonFlags_AllowOverlap;
1568#endif
1569
1570 bool hovered, held;
1571 ImRect bb_interact = bb;
1572 bb_interact.Expand(amount: axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1573 ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
1574 if (hovered)
1575 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1576
1577 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1578 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1579
1580 ImRect bb_render = bb;
1581 if (held)
1582 {
1583 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1584
1585 // Minimum pane size
1586 float size_1_maximum_delta = ImMax(lhs: 0.0f, rhs: *size1 - min_size1);
1587 float size_2_maximum_delta = ImMax(lhs: 0.0f, rhs: *size2 - min_size2);
1588 if (mouse_delta < -size_1_maximum_delta)
1589 mouse_delta = -size_1_maximum_delta;
1590 if (mouse_delta > size_2_maximum_delta)
1591 mouse_delta = size_2_maximum_delta;
1592
1593 // Apply resize
1594 if (mouse_delta != 0.0f)
1595 {
1596 *size1 = ImMax(lhs: *size1 + mouse_delta, rhs: min_size1);
1597 *size2 = ImMax(lhs: *size2 - mouse_delta, rhs: min_size2);
1598 bb_render.Translate(d: (axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1599 MarkItemEdited(id);
1600 }
1601 }
1602
1603 // Render at new position
1604 if (bg_col & IM_COL32_A_MASK)
1605 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col: bg_col, rounding: 0.0f);
1606 const ImU32 col = GetColorU32(idx: held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1607 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col, rounding: 0.0f);
1608
1609 return held;
1610}
1611
1612static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1613{
1614 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1615 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1616 if (int d = (int)(b->Width - a->Width))
1617 return d;
1618 return (b->Index - a->Index);
1619}
1620
1621// Shrink excess width from a set of item, by removing width from the larger items first.
1622// Set items Width to -1.0f to disable shrinking this item.
1623void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1624{
1625 if (count == 1)
1626 {
1627 if (items[0].Width >= 0.0f)
1628 items[0].Width = ImMax(lhs: items[0].Width - width_excess, rhs: 1.0f);
1629 return;
1630 }
1631 ImQsort(base: items, count: (size_t)count, size_of_element: sizeof(ImGuiShrinkWidthItem), compare_func: ShrinkWidthItemComparer);
1632 int count_same_width = 1;
1633 while (width_excess > 0.0f && count_same_width < count)
1634 {
1635 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1636 count_same_width++;
1637 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);
1638 if (max_width_to_remove_per_item <= 0.0f)
1639 break;
1640 float width_to_remove_per_item = ImMin(lhs: width_excess / count_same_width, rhs: max_width_to_remove_per_item);
1641 for (int item_n = 0; item_n < count_same_width; item_n++)
1642 items[item_n].Width -= width_to_remove_per_item;
1643 width_excess -= width_to_remove_per_item * count_same_width;
1644 }
1645
1646 // Round width and redistribute remainder
1647 // 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.
1648 width_excess = 0.0f;
1649 for (int n = 0; n < count; n++)
1650 {
1651 float width_rounded = ImTrunc(f: items[n].Width);
1652 width_excess += items[n].Width - width_rounded;
1653 items[n].Width = width_rounded;
1654 }
1655 while (width_excess > 0.0f)
1656 for (int n = 0; n < count && width_excess > 0.0f; n++)
1657 {
1658 float width_to_add = ImMin(lhs: items[n].InitialWidth - items[n].Width, rhs: 1.0f);
1659 items[n].Width += width_to_add;
1660 width_excess -= width_to_add;
1661 }
1662}
1663
1664//-------------------------------------------------------------------------
1665// [SECTION] Widgets: ComboBox
1666//-------------------------------------------------------------------------
1667// - CalcMaxPopupHeightFromItemCount() [Internal]
1668// - BeginCombo()
1669// - BeginComboPopup() [Internal]
1670// - EndCombo()
1671// - BeginComboPreview() [Internal]
1672// - EndComboPreview() [Internal]
1673// - Combo()
1674//-------------------------------------------------------------------------
1675
1676static float CalcMaxPopupHeightFromItemCount(int items_count)
1677{
1678 ImGuiContext& g = *GImGui;
1679 if (items_count <= 0)
1680 return FLT_MAX;
1681 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1682}
1683
1684bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1685{
1686 ImGuiContext& g = *GImGui;
1687 ImGuiWindow* window = GetCurrentWindow();
1688
1689 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
1690 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1691 if (window->SkipItems)
1692 return false;
1693
1694 const ImGuiStyle& style = g.Style;
1695 const ImGuiID id = window->GetID(str: label);
1696 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1697 if (flags & ImGuiComboFlags_WidthFitPreview)
1698 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1699
1700 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1701 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1702 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(text: preview_value, NULL, hide_text_after_double_hash: true).x : 0.0f;
1703 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1704 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1705 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1706 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1707 if (!ItemAdd(bb: total_bb, id, nav_bb: &bb))
1708 return false;
1709
1710 // Open on click
1711 bool hovered, held;
1712 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1713 const ImGuiID popup_id = ImHashStr(data: "##ComboPopup", data_size: 0, seed: id);
1714 bool popup_open = IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1715 if (pressed && !popup_open)
1716 {
1717 OpenPopupEx(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1718 popup_open = true;
1719 }
1720
1721 // Render shape
1722 const ImU32 frame_col = GetColorU32(idx: hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1723 const float value_x2 = ImMax(lhs: bb.Min.x, rhs: bb.Max.x - arrow_size);
1724 RenderNavHighlight(bb, id);
1725 if (!(flags & ImGuiComboFlags_NoPreview))
1726 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);
1727 if (!(flags & ImGuiComboFlags_NoArrowButton))
1728 {
1729 ImU32 bg_col = GetColorU32(idx: (popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1730 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1731 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);
1732 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1733 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);
1734 }
1735 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding: style.FrameRounding);
1736
1737 // Custom preview
1738 if (flags & ImGuiComboFlags_CustomPreview)
1739 {
1740 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1741 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1742 preview_value = NULL;
1743 }
1744
1745 // Render preview and label
1746 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1747 {
1748 if (g.LogEnabled)
1749 LogSetNextTextDecoration(prefix: "{", suffix: "}");
1750 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: ImVec2(value_x2, bb.Max.y), text: preview_value, NULL, NULL);
1751 }
1752 if (label_size.x > 0)
1753 RenderText(pos: ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), text: label);
1754
1755 if (!popup_open)
1756 return false;
1757
1758 g.NextWindowData.Flags = backup_next_window_data_flags;
1759 return BeginComboPopup(popup_id, bb, flags);
1760}
1761
1762bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1763{
1764 ImGuiContext& g = *GImGui;
1765 if (!IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None))
1766 {
1767 g.NextWindowData.ClearFlags();
1768 return false;
1769 }
1770
1771 // Set popup size
1772 float w = bb.GetWidth();
1773 if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1774 {
1775 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(lhs: g.NextWindowData.SizeConstraintRect.Min.x, rhs: w);
1776 }
1777 else
1778 {
1779 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1780 flags |= ImGuiComboFlags_HeightRegular;
1781 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1782 int popup_max_height_in_items = -1;
1783 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1784 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1785 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1786 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1787 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1788 constraint_min.x = w;
1789 if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
1790 constraint_max.y = CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items);
1791 SetNextWindowSizeConstraints(size_min: constraint_min, size_max: constraint_max);
1792 }
1793
1794 // This is essentially a specialized version of BeginPopupEx()
1795 char name[16];
1796 ImFormatString(buf: name, IM_ARRAYSIZE(name), fmt: "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
1797
1798 // Set position given a custom constraint (peak into expected window size so we can position it)
1799 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1800 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1801 if (ImGuiWindow* popup_window = FindWindowByName(name))
1802 if (popup_window->WasActive)
1803 {
1804 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1805 ImVec2 size_expected = CalcWindowNextAutoFitSize(window: popup_window);
1806 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1807 ImRect r_outer = GetPopupAllowedExtentRect(window: popup_window);
1808 ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos: bb.GetBL(), size: size_expected, last_dir: &popup_window->AutoPosLastDirection, r_outer, r_avoid: bb, policy: ImGuiPopupPositionPolicy_ComboBox);
1809 SetNextWindowPos(pos);
1810 }
1811
1812 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1813 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1814 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text
1815 bool ret = Begin(name, NULL, flags: window_flags);
1816 PopStyleVar();
1817 if (!ret)
1818 {
1819 EndPopup();
1820 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1821 return false;
1822 }
1823 g.BeginComboDepth++;
1824 return true;
1825}
1826
1827void ImGui::EndCombo()
1828{
1829 ImGuiContext& g = *GImGui;
1830 EndPopup();
1831 g.BeginComboDepth--;
1832}
1833
1834// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
1835// (Experimental, see GitHub issues: #1658, #4168)
1836bool ImGui::BeginComboPreview()
1837{
1838 ImGuiContext& g = *GImGui;
1839 ImGuiWindow* window = g.CurrentWindow;
1840 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1841
1842 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
1843 return false;
1844 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?
1845 if (!window->ClipRect.Overlaps(r: preview_data->PreviewRect)) // Narrower test (optional)
1846 return false;
1847
1848 // FIXME: This could be contained in a PushWorkRect() api
1849 preview_data->BackupCursorPos = window->DC.CursorPos;
1850 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
1851 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
1852 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
1853 preview_data->BackupLayout = window->DC.LayoutType;
1854 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
1855 window->DC.CursorMaxPos = window->DC.CursorPos;
1856 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
1857 window->DC.IsSameLine = false;
1858 PushClipRect(clip_rect_min: preview_data->PreviewRect.Min, clip_rect_max: preview_data->PreviewRect.Max, intersect_with_current_clip_rect: true);
1859
1860 return true;
1861}
1862
1863void ImGui::EndComboPreview()
1864{
1865 ImGuiContext& g = *GImGui;
1866 ImGuiWindow* window = g.CurrentWindow;
1867 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1868
1869 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
1870 ImDrawList* draw_list = window->DrawList;
1871 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
1872 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
1873 {
1874 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
1875 draw_list->_TryMergeDrawCmds();
1876 }
1877 PopClipRect();
1878 window->DC.CursorPos = preview_data->BackupCursorPos;
1879 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: preview_data->BackupCursorMaxPos);
1880 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
1881 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
1882 window->DC.LayoutType = preview_data->BackupLayout;
1883 window->DC.IsSameLine = false;
1884 preview_data->PreviewRect = ImRect();
1885}
1886
1887// Getter for the old Combo() API: const char*[]
1888static const char* Items_ArrayGetter(void* data, int idx)
1889{
1890 const char* const* items = (const char* const*)data;
1891 return items[idx];
1892}
1893
1894// Getter for the old Combo() API: "item1\0item2\0item3\0"
1895static const char* Items_SingleStringGetter(void* data, int idx)
1896{
1897 const char* items_separated_by_zeros = (const char*)data;
1898 int items_count = 0;
1899 const char* p = items_separated_by_zeros;
1900 while (*p)
1901 {
1902 if (idx == items_count)
1903 break;
1904 p += strlen(s: p) + 1;
1905 items_count++;
1906 }
1907 return *p ? p : NULL;
1908}
1909
1910// Old API, prefer using BeginCombo() nowadays if you can.
1911bool 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)
1912{
1913 ImGuiContext& g = *GImGui;
1914
1915 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
1916 const char* preview_value = NULL;
1917 if (*current_item >= 0 && *current_item < items_count)
1918 preview_value = getter(user_data, *current_item);
1919
1920 // 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.
1921 if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
1922 SetNextWindowSizeConstraints(size_min: ImVec2(0, 0), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
1923
1924 if (!BeginCombo(label, preview_value, flags: ImGuiComboFlags_None))
1925 return false;
1926
1927 // Display items
1928 // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
1929 bool value_changed = false;
1930 for (int i = 0; i < items_count; i++)
1931 {
1932 const char* item_text = getter(user_data, i);
1933 if (item_text == NULL)
1934 item_text = "*Unknown item*";
1935
1936 PushID(int_id: i);
1937 const bool item_selected = (i == *current_item);
1938 if (Selectable(label: item_text, selected: item_selected) && *current_item != i)
1939 {
1940 value_changed = true;
1941 *current_item = i;
1942 }
1943 if (item_selected)
1944 SetItemDefaultFocus();
1945 PopID();
1946 }
1947
1948 EndCombo();
1949
1950 if (value_changed)
1951 MarkItemEdited(id: g.LastItemData.ID);
1952
1953 return value_changed;
1954}
1955
1956// Combo box helper allowing to pass an array of strings.
1957bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
1958{
1959 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);
1960 return value_changed;
1961}
1962
1963// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
1964bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
1965{
1966 int items_count = 0;
1967 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
1968 while (*p)
1969 {
1970 p += strlen(s: p) + 1;
1971 items_count++;
1972 }
1973 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);
1974 return value_changed;
1975}
1976
1977#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1978
1979struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
1980static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
1981{
1982 ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
1983 const char* s = NULL;
1984 data->OldCallback(data->UserData, idx, &s);
1985 return s;
1986}
1987
1988bool 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)
1989{
1990 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
1991 return ListBox(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, height_in_items);
1992}
1993bool 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)
1994{
1995 ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { .UserData: user_data, .OldCallback: old_getter };
1996 return Combo(label, current_item, getter: ImGuiGetNameFromIndexOldToNewCallback, user_data: &old_to_new_data, items_count, popup_max_height_in_items);
1997}
1998
1999#endif
2000
2001//-------------------------------------------------------------------------
2002// [SECTION] Data Type and Data Formatting Helpers [Internal]
2003//-------------------------------------------------------------------------
2004// - DataTypeGetInfo()
2005// - DataTypeFormatString()
2006// - DataTypeApplyOp()
2007// - DataTypeApplyFromText()
2008// - DataTypeCompare()
2009// - DataTypeClamp()
2010// - GetMinimumStepAtDecimalPrecision
2011// - RoundScalarWithFormat<>()
2012//-------------------------------------------------------------------------
2013
2014static const ImGuiDataTypeInfo GDataTypeInfo[] =
2015{
2016 { .Size: sizeof(char), .Name: "S8", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S8
2017 { .Size: sizeof(unsigned char), .Name: "U8", .PrintFmt: "%u", .ScanFmt: "%u" },
2018 { .Size: sizeof(short), .Name: "S16", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S16
2019 { .Size: sizeof(unsigned short), .Name: "U16", .PrintFmt: "%u", .ScanFmt: "%u" },
2020 { .Size: sizeof(int), .Name: "S32", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S32
2021 { .Size: sizeof(unsigned int), .Name: "U32", .PrintFmt: "%u", .ScanFmt: "%u" },
2022#ifdef _MSC_VER
2023 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2024 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2025#else
2026 { .Size: sizeof(ImS64), .Name: "S64", .PrintFmt: "%lld", .ScanFmt: "%lld" }, // ImGuiDataType_S64
2027 { .Size: sizeof(ImU64), .Name: "U64", .PrintFmt: "%llu", .ScanFmt: "%llu" },
2028#endif
2029 { .Size: sizeof(float), .Name: "float", .PrintFmt: "%.3f",.ScanFmt: "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2030 { .Size: sizeof(double), .Name: "double",.PrintFmt: "%f", .ScanFmt: "%lf" }, // ImGuiDataType_Double
2031};
2032IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2033
2034const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2035{
2036 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2037 return &GDataTypeInfo[data_type];
2038}
2039
2040int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2041{
2042 // Signedness doesn't matter when pushing integer arguments
2043 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2044 return ImFormatString(buf, buf_size, fmt: format, *(const ImU32*)p_data);
2045 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2046 return ImFormatString(buf, buf_size, fmt: format, *(const ImU64*)p_data);
2047 if (data_type == ImGuiDataType_Float)
2048 return ImFormatString(buf, buf_size, fmt: format, *(const float*)p_data);
2049 if (data_type == ImGuiDataType_Double)
2050 return ImFormatString(buf, buf_size, fmt: format, *(const double*)p_data);
2051 if (data_type == ImGuiDataType_S8)
2052 return ImFormatString(buf, buf_size, fmt: format, *(const ImS8*)p_data);
2053 if (data_type == ImGuiDataType_U8)
2054 return ImFormatString(buf, buf_size, fmt: format, *(const ImU8*)p_data);
2055 if (data_type == ImGuiDataType_S16)
2056 return ImFormatString(buf, buf_size, fmt: format, *(const ImS16*)p_data);
2057 if (data_type == ImGuiDataType_U16)
2058 return ImFormatString(buf, buf_size, fmt: format, *(const ImU16*)p_data);
2059 IM_ASSERT(0);
2060 return 0;
2061}
2062
2063void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2064{
2065 IM_ASSERT(op == '+' || op == '-');
2066 switch (data_type)
2067 {
2068 case ImGuiDataType_S8:
2069 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2070 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
2071 return;
2072 case ImGuiDataType_U8:
2073 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2074 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
2075 return;
2076 case ImGuiDataType_S16:
2077 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2078 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
2079 return;
2080 case ImGuiDataType_U16:
2081 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2082 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
2083 return;
2084 case ImGuiDataType_S32:
2085 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2086 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
2087 return;
2088 case ImGuiDataType_U32:
2089 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2090 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
2091 return;
2092 case ImGuiDataType_S64:
2093 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2094 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
2095 return;
2096 case ImGuiDataType_U64:
2097 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2098 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
2099 return;
2100 case ImGuiDataType_Float:
2101 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2102 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2103 return;
2104 case ImGuiDataType_Double:
2105 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2106 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2107 return;
2108 case ImGuiDataType_COUNT: break;
2109 }
2110 IM_ASSERT(0);
2111}
2112
2113// User can input math operators (e.g. +100) to edit a numerical values.
2114// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2115bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format)
2116{
2117 while (ImCharIsBlankA(c: *buf))
2118 buf++;
2119 if (!buf[0])
2120 return false;
2121
2122 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2123 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2124 ImGuiDataTypeTempStorage data_backup;
2125 memcpy(dest: &data_backup, src: p_data, n: type_info->Size);
2126
2127 // Sanitize format
2128 // - 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
2129 // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2130 char format_sanitized[32];
2131 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2132 format = type_info->ScanFmt;
2133 else
2134 format = ImParseFormatSanitizeForScanning(fmt_in: format, fmt_out: format_sanitized, IM_ARRAYSIZE(format_sanitized));
2135
2136 // Small types need a 32-bit buffer to receive the result from scanf()
2137 int v32 = 0;
2138 if (sscanf(s: buf, format: format, type_info->Size >= 4 ? p_data : &v32) < 1)
2139 return false;
2140 if (type_info->Size < 4)
2141 {
2142 if (data_type == ImGuiDataType_S8)
2143 *(ImS8*)p_data = (ImS8)ImClamp(v: v32, mn: (int)IM_S8_MIN, mx: (int)IM_S8_MAX);
2144 else if (data_type == ImGuiDataType_U8)
2145 *(ImU8*)p_data = (ImU8)ImClamp(v: v32, mn: (int)IM_U8_MIN, mx: (int)IM_U8_MAX);
2146 else if (data_type == ImGuiDataType_S16)
2147 *(ImS16*)p_data = (ImS16)ImClamp(v: v32, mn: (int)IM_S16_MIN, mx: (int)IM_S16_MAX);
2148 else if (data_type == ImGuiDataType_U16)
2149 *(ImU16*)p_data = (ImU16)ImClamp(v: v32, mn: (int)IM_U16_MIN, mx: (int)IM_U16_MAX);
2150 else
2151 IM_ASSERT(0);
2152 }
2153
2154 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2155}
2156
2157template<typename T>
2158static int DataTypeCompareT(const T* lhs, const T* rhs)
2159{
2160 if (*lhs < *rhs) return -1;
2161 if (*lhs > *rhs) return +1;
2162 return 0;
2163}
2164
2165int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2166{
2167 switch (data_type)
2168 {
2169 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >(lhs: (const ImS8* )arg_1, rhs: (const ImS8* )arg_2);
2170 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >(lhs: (const ImU8* )arg_1, rhs: (const ImU8* )arg_2);
2171 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >(lhs: (const ImS16* )arg_1, rhs: (const ImS16* )arg_2);
2172 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >(lhs: (const ImU16* )arg_1, rhs: (const ImU16* )arg_2);
2173 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >(lhs: (const ImS32* )arg_1, rhs: (const ImS32* )arg_2);
2174 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >(lhs: (const ImU32* )arg_1, rhs: (const ImU32* )arg_2);
2175 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >(lhs: (const ImS64* )arg_1, rhs: (const ImS64* )arg_2);
2176 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >(lhs: (const ImU64* )arg_1, rhs: (const ImU64* )arg_2);
2177 case ImGuiDataType_Float: return DataTypeCompareT<float >(lhs: (const float* )arg_1, rhs: (const float* )arg_2);
2178 case ImGuiDataType_Double: return DataTypeCompareT<double>(lhs: (const double*)arg_1, rhs: (const double*)arg_2);
2179 case ImGuiDataType_COUNT: break;
2180 }
2181 IM_ASSERT(0);
2182 return 0;
2183}
2184
2185template<typename T>
2186static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2187{
2188 // Clamp, both sides are optional, return true if modified
2189 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2190 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2191 return false;
2192}
2193
2194bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2195{
2196 switch (data_type)
2197 {
2198 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >(v: (ImS8* )p_data, v_min: (const ImS8* )p_min, v_max: (const ImS8* )p_max);
2199 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >(v: (ImU8* )p_data, v_min: (const ImU8* )p_min, v_max: (const ImU8* )p_max);
2200 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >(v: (ImS16* )p_data, v_min: (const ImS16* )p_min, v_max: (const ImS16* )p_max);
2201 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >(v: (ImU16* )p_data, v_min: (const ImU16* )p_min, v_max: (const ImU16* )p_max);
2202 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >(v: (ImS32* )p_data, v_min: (const ImS32* )p_min, v_max: (const ImS32* )p_max);
2203 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >(v: (ImU32* )p_data, v_min: (const ImU32* )p_min, v_max: (const ImU32* )p_max);
2204 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >(v: (ImS64* )p_data, v_min: (const ImS64* )p_min, v_max: (const ImS64* )p_max);
2205 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >(v: (ImU64* )p_data, v_min: (const ImU64* )p_min, v_max: (const ImU64* )p_max);
2206 case ImGuiDataType_Float: return DataTypeClampT<float >(v: (float* )p_data, v_min: (const float* )p_min, v_max: (const float* )p_max);
2207 case ImGuiDataType_Double: return DataTypeClampT<double>(v: (double*)p_data, v_min: (const double*)p_min, v_max: (const double*)p_max);
2208 case ImGuiDataType_COUNT: break;
2209 }
2210 IM_ASSERT(0);
2211 return false;
2212}
2213
2214static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2215{
2216 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 };
2217 if (decimal_precision < 0)
2218 return FLT_MIN;
2219 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(x: 10.0f, y: (float)-decimal_precision);
2220}
2221
2222template<typename TYPE>
2223TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2224{
2225 IM_UNUSED(data_type);
2226 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2227 const char* fmt_start = ImParseFormatFindStart(format);
2228 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2229 return v;
2230
2231 // Sanitize format
2232 char fmt_sanitized[32];
2233 ImParseFormatSanitizeForPrinting(fmt_in: fmt_start, fmt_out: fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2234 fmt_start = fmt_sanitized;
2235
2236 // Format value with our rounding, and read back
2237 char v_str[64];
2238 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2239 const char* p = v_str;
2240 while (*p == ' ')
2241 p++;
2242 v = (TYPE)ImAtof(p);
2243
2244 return v;
2245}
2246
2247//-------------------------------------------------------------------------
2248// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2249//-------------------------------------------------------------------------
2250// - DragBehaviorT<>() [Internal]
2251// - DragBehavior() [Internal]
2252// - DragScalar()
2253// - DragScalarN()
2254// - DragFloat()
2255// - DragFloat2()
2256// - DragFloat3()
2257// - DragFloat4()
2258// - DragFloatRange2()
2259// - DragInt()
2260// - DragInt2()
2261// - DragInt3()
2262// - DragInt4()
2263// - DragIntRange2()
2264//-------------------------------------------------------------------------
2265
2266// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2267template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2268bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2269{
2270 ImGuiContext& g = *GImGui;
2271 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2272 const bool is_clamped = (v_min < v_max);
2273 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2274 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2275
2276 // Default tweak speed
2277 if (v_speed == 0.0f && is_clamped && (v_max - v_min < FLT_MAX))
2278 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2279
2280 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2281 float adjust_delta = 0.0f;
2282 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2283 {
2284 adjust_delta = g.IO.MouseDelta[axis];
2285 if (g.IO.KeyAlt)
2286 adjust_delta *= 1.0f / 100.0f;
2287 if (g.IO.KeyShift)
2288 adjust_delta *= 10.0f;
2289 }
2290 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2291 {
2292 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2293 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2294 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2295 const float tweak_factor = tweak_slow ? 1.0f / 1.0f : tweak_fast ? 10.0f : 1.0f;
2296 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2297 v_speed = ImMax(lhs: v_speed, rhs: GetMinimumStepAtDecimalPrecision(decimal_precision));
2298 }
2299 adjust_delta *= v_speed;
2300
2301 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2302 if (axis == ImGuiAxis_Y)
2303 adjust_delta = -adjust_delta;
2304
2305 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2306 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2307 adjust_delta /= (float)(v_max - v_min);
2308
2309 // Clear current value on activation
2310 // 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.
2311 bool is_just_activated = g.ActiveIdIsJustActivated;
2312 bool is_already_past_limits_and_pushing_outward = is_clamped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f));
2313 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2314 {
2315 g.DragCurrentAccum = 0.0f;
2316 g.DragCurrentAccumDirty = false;
2317 }
2318 else if (adjust_delta != 0.0f)
2319 {
2320 g.DragCurrentAccum += adjust_delta;
2321 g.DragCurrentAccumDirty = true;
2322 }
2323
2324 if (!g.DragCurrentAccumDirty)
2325 return false;
2326
2327 TYPE v_cur = *v;
2328 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2329
2330 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2331 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2332 if (is_logarithmic)
2333 {
2334 // 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.
2335 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2336 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2337
2338 // Convert to parametric space, apply delta, convert back
2339 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2340 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2341 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2342 v_old_ref_for_accum_remainder = v_old_parametric;
2343 }
2344 else
2345 {
2346 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2347 }
2348
2349 // Round to user desired precision based on format string
2350 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2351 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2352
2353 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2354 g.DragCurrentAccumDirty = false;
2355 if (is_logarithmic)
2356 {
2357 // Convert to parametric space, apply delta, convert back
2358 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2359 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2360 }
2361 else
2362 {
2363 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2364 }
2365
2366 // Lose zero sign for float/double
2367 if (v_cur == (TYPE)-0)
2368 v_cur = (TYPE)0;
2369
2370 // Clamp values (+ handle overflow/wrap-around for integer types)
2371 if (*v != v_cur && is_clamped)
2372 {
2373 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2374 v_cur = v_min;
2375 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2376 v_cur = v_max;
2377 }
2378
2379 // Apply result
2380 if (*v == v_cur)
2381 return false;
2382 *v = v_cur;
2383 return true;
2384}
2385
2386bool 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)
2387{
2388 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2389 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
2390
2391 ImGuiContext& g = *GImGui;
2392 if (g.ActiveId == id)
2393 {
2394 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2395 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2396 ClearActiveID();
2397 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2398 ClearActiveID();
2399 }
2400 if (g.ActiveId != id)
2401 return false;
2402 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2403 return false;
2404
2405 switch (data_type)
2406 {
2407 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; }
2408 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; }
2409 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; }
2410 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; }
2411 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);
2412 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);
2413 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);
2414 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);
2415 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);
2416 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);
2417 case ImGuiDataType_COUNT: break;
2418 }
2419 IM_ASSERT(0);
2420 return false;
2421}
2422
2423// 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.
2424// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2425bool 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)
2426{
2427 ImGuiWindow* window = GetCurrentWindow();
2428 if (window->SkipItems)
2429 return false;
2430
2431 ImGuiContext& g = *GImGui;
2432 const ImGuiStyle& style = g.Style;
2433 const ImGuiID id = window->GetID(str: label);
2434 const float w = CalcItemWidth();
2435
2436 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2437 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2438 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));
2439
2440 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2441 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2442 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2443 return false;
2444
2445 // Default format string when passing NULL
2446 if (format == NULL)
2447 format = DataTypeGetInfo(data_type)->PrintFmt;
2448
2449 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
2450 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2451 if (!temp_input_is_active)
2452 {
2453 // Tabbing or CTRL-clicking on Drag turns it into an InputText
2454 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
2455 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id));
2456 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2457 if (make_active && (clicked || double_clicked))
2458 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2459 if (make_active && temp_input_allowed)
2460 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2461 temp_input_is_active = true;
2462
2463 // (Optional) simple click (without moving) turns Drag into an InputText
2464 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2465 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2466 {
2467 g.NavActivateId = id;
2468 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2469 temp_input_is_active = true;
2470 }
2471
2472 if (make_active && !temp_input_is_active)
2473 {
2474 SetActiveID(id, window);
2475 SetFocusID(id, window);
2476 FocusWindow(window);
2477 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2478 }
2479 }
2480
2481 if (temp_input_is_active)
2482 {
2483 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
2484 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);
2485 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);
2486 }
2487
2488 // Draw frame
2489 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2490 RenderNavHighlight(bb: frame_bb, id);
2491 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: style.FrameRounding);
2492
2493 // Drag behavior
2494 const bool value_changed = DragBehavior(id, data_type, p_v: p_data, v_speed, p_min, p_max, format, flags);
2495 if (value_changed)
2496 MarkItemEdited(id);
2497
2498 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2499 char value_buf[64];
2500 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2501 if (g.LogEnabled)
2502 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2503 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));
2504
2505 if (label_size.x > 0.0f)
2506 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2507
2508 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2509 return value_changed;
2510}
2511
2512bool 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)
2513{
2514 ImGuiWindow* window = GetCurrentWindow();
2515 if (window->SkipItems)
2516 return false;
2517
2518 ImGuiContext& g = *GImGui;
2519 bool value_changed = false;
2520 BeginGroup();
2521 PushID(str_id: label);
2522 PushMultiItemsWidths(components, width_full: CalcItemWidth());
2523 size_t type_size = GDataTypeInfo[data_type].Size;
2524 for (int i = 0; i < components; i++)
2525 {
2526 PushID(int_id: i);
2527 if (i > 0)
2528 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2529 value_changed |= DragScalar(label: "", data_type, p_data, v_speed, p_min, p_max, format, flags);
2530 PopID();
2531 PopItemWidth();
2532 p_data = (void*)((char*)p_data + type_size);
2533 }
2534 PopID();
2535
2536 const char* label_end = FindRenderedTextEnd(text: label);
2537 if (label != label_end)
2538 {
2539 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2540 TextEx(text: label, text_end: label_end);
2541 }
2542
2543 EndGroup();
2544 return value_changed;
2545}
2546
2547bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2548{
2549 return DragScalar(label, data_type: ImGuiDataType_Float, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2550}
2551
2552bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2553{
2554 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2555}
2556
2557bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2558{
2559 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2560}
2561
2562bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2563{
2564 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2565}
2566
2567// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2568bool 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)
2569{
2570 ImGuiWindow* window = GetCurrentWindow();
2571 if (window->SkipItems)
2572 return false;
2573
2574 ImGuiContext& g = *GImGui;
2575 PushID(str_id: label);
2576 BeginGroup();
2577 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2578
2579 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2580 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2581 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2582 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);
2583 PopItemWidth();
2584 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2585
2586 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2587 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2588 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2589 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);
2590 PopItemWidth();
2591 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2592
2593 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2594 EndGroup();
2595 PopID();
2596
2597 return value_changed;
2598}
2599
2600// NB: v_speed is float to allow adjusting the drag speed with more precision
2601bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2602{
2603 return DragScalar(label, data_type: ImGuiDataType_S32, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2604}
2605
2606bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2607{
2608 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2609}
2610
2611bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2612{
2613 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2614}
2615
2616bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2617{
2618 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2619}
2620
2621// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2622bool 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)
2623{
2624 ImGuiWindow* window = GetCurrentWindow();
2625 if (window->SkipItems)
2626 return false;
2627
2628 ImGuiContext& g = *GImGui;
2629 PushID(str_id: label);
2630 BeginGroup();
2631 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2632
2633 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2634 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2635 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2636 bool value_changed = DragInt(label: "##min", v: v_current_min, v_speed, v_min: min_min, v_max: min_max, format, flags: min_flags);
2637 PopItemWidth();
2638 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2639
2640 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2641 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2642 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2643 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);
2644 PopItemWidth();
2645 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2646
2647 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2648 EndGroup();
2649 PopID();
2650
2651 return value_changed;
2652}
2653
2654//-------------------------------------------------------------------------
2655// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2656//-------------------------------------------------------------------------
2657// - ScaleRatioFromValueT<> [Internal]
2658// - ScaleValueFromRatioT<> [Internal]
2659// - SliderBehaviorT<>() [Internal]
2660// - SliderBehavior() [Internal]
2661// - SliderScalar()
2662// - SliderScalarN()
2663// - SliderFloat()
2664// - SliderFloat2()
2665// - SliderFloat3()
2666// - SliderFloat4()
2667// - SliderAngle()
2668// - SliderInt()
2669// - SliderInt2()
2670// - SliderInt3()
2671// - SliderInt4()
2672// - VSliderScalar()
2673// - VSliderFloat()
2674// - VSliderInt()
2675//-------------------------------------------------------------------------
2676
2677// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2678template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2679float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2680{
2681 if (v_min == v_max)
2682 return 0.0f;
2683 IM_UNUSED(data_type);
2684
2685 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2686 if (is_logarithmic)
2687 {
2688 bool flipped = v_max < v_min;
2689
2690 if (flipped) // Handle the case where the range is backwards
2691 ImSwap(v_min, v_max);
2692
2693 // Fudge min/max to avoid getting close to log(0)
2694 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2695 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2696
2697 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2698 if ((v_min == 0.0f) && (v_max < 0.0f))
2699 v_min_fudged = -logarithmic_zero_epsilon;
2700 else if ((v_max == 0.0f) && (v_min < 0.0f))
2701 v_max_fudged = -logarithmic_zero_epsilon;
2702
2703 float result;
2704 if (v_clamped <= v_min_fudged)
2705 result = 0.0f; // Workaround for values that are in-range but below our fudge
2706 else if (v_clamped >= v_max_fudged)
2707 result = 1.0f; // Workaround for values that are in-range but above our fudge
2708 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2709 {
2710 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)
2711 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2712 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2713 if (v == 0.0f)
2714 result = zero_point_center; // Special case for exactly zero
2715 else if (v < 0.0f)
2716 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2717 else
2718 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));
2719 }
2720 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2721 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2722 else
2723 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2724
2725 return flipped ? (1.0f - result) : result;
2726 }
2727 else
2728 {
2729 // Linear slider
2730 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2731 }
2732}
2733
2734// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2735template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2736TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2737{
2738 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2739 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2740 if (t <= 0.0f || v_min == v_max)
2741 return v_min;
2742 if (t >= 1.0f)
2743 return v_max;
2744
2745 TYPE result = (TYPE)0;
2746 if (is_logarithmic)
2747 {
2748 // Fudge min/max to avoid getting silly results close to zero
2749 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2750 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2751
2752 const bool flipped = v_max < v_min; // Check if range is "backwards"
2753 if (flipped)
2754 ImSwap(v_min_fudged, v_max_fudged);
2755
2756 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2757 if ((v_max == 0.0f) && (v_min < 0.0f))
2758 v_max_fudged = -logarithmic_zero_epsilon;
2759
2760 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2761
2762 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2763 {
2764 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs(x: (float)v_max - (float)v_min); // The zero point in parametric space
2765 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2766 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2767 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2768 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2769 else if (t_with_flip < zero_point_center)
2770 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2771 else
2772 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))));
2773 }
2774 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2775 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2776 else
2777 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2778 }
2779 else
2780 {
2781 // Linear slider
2782 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2783 if (is_floating_point)
2784 {
2785 result = ImLerp(v_min, v_max, t);
2786 }
2787 else if (t < 1.0)
2788 {
2789 // - For integer values we want the clicking position to match the grab box so we round above
2790 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
2791 // - 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
2792 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
2793 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
2794 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
2795 }
2796 }
2797
2798 return result;
2799}
2800
2801// FIXME: Try to move more of the code into shared SliderBehavior()
2802template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2803bool 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)
2804{
2805 ImGuiContext& g = *GImGui;
2806 const ImGuiStyle& style = g.Style;
2807
2808 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2809 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2810 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2811 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.
2812
2813 // Calculate bounds
2814 const float grab_padding = 2.0f; // FIXME: Should be part of style.
2815 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
2816 float grab_sz = style.GrabMinSize;
2817 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
2818 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
2819 grab_sz = ImMin(lhs: grab_sz, rhs: slider_sz);
2820 const float slider_usable_sz = slider_sz - grab_sz;
2821 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
2822 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
2823
2824 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2825 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
2826 if (is_logarithmic)
2827 {
2828 // 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.
2829 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2830 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2831 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(lhs: slider_usable_sz, rhs: 1.0f);
2832 }
2833
2834 // Process interacting with the slider
2835 bool value_changed = false;
2836 if (g.ActiveId == id)
2837 {
2838 bool set_new_value = false;
2839 float clicked_t = 0.0f;
2840 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
2841 {
2842 if (!g.IO.MouseDown[0])
2843 {
2844 ClearActiveID();
2845 }
2846 else
2847 {
2848 const float mouse_abs_pos = g.IO.MousePos[axis];
2849 if (g.ActiveIdIsJustActivated)
2850 {
2851 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2852 if (axis == ImGuiAxis_Y)
2853 grab_t = 1.0f - grab_t;
2854 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
2855 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.
2856 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
2857 }
2858 if (slider_usable_sz > 0.0f)
2859 clicked_t = ImSaturate(f: (mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
2860 if (axis == ImGuiAxis_Y)
2861 clicked_t = 1.0f - clicked_t;
2862 set_new_value = true;
2863 }
2864 }
2865 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2866 {
2867 if (g.ActiveIdIsJustActivated)
2868 {
2869 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
2870 g.SliderCurrentAccumDirty = false;
2871 }
2872
2873 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
2874 if (input_delta != 0.0f)
2875 {
2876 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2877 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2878 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2879 if (decimal_precision > 0)
2880 {
2881 input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds
2882 if (tweak_slow)
2883 input_delta /= 10.0f;
2884 }
2885 else
2886 {
2887 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
2888 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Gamepad/keyboard tweak speeds in integer steps
2889 else
2890 input_delta /= 100.0f;
2891 }
2892 if (tweak_fast)
2893 input_delta *= 10.0f;
2894
2895 g.SliderCurrentAccum += input_delta;
2896 g.SliderCurrentAccumDirty = true;
2897 }
2898
2899 float delta = g.SliderCurrentAccum;
2900 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2901 {
2902 ClearActiveID();
2903 }
2904 else if (g.SliderCurrentAccumDirty)
2905 {
2906 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2907
2908 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
2909 {
2910 set_new_value = false;
2911 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
2912 }
2913 else
2914 {
2915 set_new_value = true;
2916 float old_clicked_t = clicked_t;
2917 clicked_t = ImSaturate(f: clicked_t + delta);
2918
2919 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
2920 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2921 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2922 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
2923 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2924
2925 if (delta > 0)
2926 g.SliderCurrentAccum -= ImMin(lhs: new_clicked_t - old_clicked_t, rhs: delta);
2927 else
2928 g.SliderCurrentAccum -= ImMax(lhs: new_clicked_t - old_clicked_t, rhs: delta);
2929 }
2930
2931 g.SliderCurrentAccumDirty = false;
2932 }
2933 }
2934
2935 if (set_new_value)
2936 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2937 set_new_value = false;
2938
2939 if (set_new_value)
2940 {
2941 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2942
2943 // Round to user desired precision based on format string
2944 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2945 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
2946
2947 // Apply result
2948 if (*v != v_new)
2949 {
2950 *v = v_new;
2951 value_changed = true;
2952 }
2953 }
2954 }
2955
2956 if (slider_sz < 1.0f)
2957 {
2958 *out_grab_bb = ImRect(bb.Min, bb.Min);
2959 }
2960 else
2961 {
2962 // Output grab position so it can be displayed by the caller
2963 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2964 if (axis == ImGuiAxis_Y)
2965 grab_t = 1.0f - grab_t;
2966 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
2967 if (axis == ImGuiAxis_X)
2968 *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);
2969 else
2970 *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);
2971 }
2972
2973 return value_changed;
2974}
2975
2976// For 32-bit and larger types, slider bounds are limited to half the natural type range.
2977// 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.
2978// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
2979bool 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)
2980{
2981 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2982 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flag! Has the 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
2983
2984 switch (data_type)
2985 {
2986 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; }
2987 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; }
2988 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; }
2989 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; }
2990 case ImGuiDataType_S32:
2991 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
2992 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);
2993 case ImGuiDataType_U32:
2994 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
2995 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);
2996 case ImGuiDataType_S64:
2997 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
2998 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);
2999 case ImGuiDataType_U64:
3000 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3001 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);
3002 case ImGuiDataType_Float:
3003 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3004 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);
3005 case ImGuiDataType_Double:
3006 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3007 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);
3008 case ImGuiDataType_COUNT: break;
3009 }
3010 IM_ASSERT(0);
3011 return false;
3012}
3013
3014// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3015// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3016bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3017{
3018 ImGuiWindow* window = GetCurrentWindow();
3019 if (window->SkipItems)
3020 return false;
3021
3022 ImGuiContext& g = *GImGui;
3023 const ImGuiStyle& style = g.Style;
3024 const ImGuiID id = window->GetID(str: label);
3025 const float w = CalcItemWidth();
3026
3027 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3028 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3029 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));
3030
3031 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3032 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
3033 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3034 return false;
3035
3036 // Default format string when passing NULL
3037 if (format == NULL)
3038 format = DataTypeGetInfo(data_type)->PrintFmt;
3039
3040 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
3041 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3042 if (!temp_input_is_active)
3043 {
3044 // Tabbing or CTRL-clicking on Slider turns it into an input box
3045 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
3046 const bool make_active = (clicked || g.NavActivateId == id);
3047 if (make_active && clicked)
3048 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3049 if (make_active && temp_input_allowed)
3050 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3051 temp_input_is_active = true;
3052
3053 if (make_active && !temp_input_is_active)
3054 {
3055 SetActiveID(id, window);
3056 SetFocusID(id, window);
3057 FocusWindow(window);
3058 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3059 }
3060 }
3061
3062 if (temp_input_is_active)
3063 {
3064 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
3065 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0;
3066 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);
3067 }
3068
3069 // Draw frame
3070 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3071 RenderNavHighlight(bb: frame_bb, id);
3072 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
3073
3074 // Slider behavior
3075 ImRect grab_bb;
3076 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);
3077 if (value_changed)
3078 MarkItemEdited(id);
3079
3080 // Render grab
3081 if (grab_bb.Max.x > grab_bb.Min.x)
3082 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);
3083
3084 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3085 char value_buf[64];
3086 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3087 if (g.LogEnabled)
3088 LogSetNextTextDecoration(prefix: "{", suffix: "}");
3089 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));
3090
3091 if (label_size.x > 0.0f)
3092 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3093
3094 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3095 return value_changed;
3096}
3097
3098// Add multiple sliders on 1 line for compact edition of multiple components
3099bool 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)
3100{
3101 ImGuiWindow* window = GetCurrentWindow();
3102 if (window->SkipItems)
3103 return false;
3104
3105 ImGuiContext& g = *GImGui;
3106 bool value_changed = false;
3107 BeginGroup();
3108 PushID(str_id: label);
3109 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3110 size_t type_size = GDataTypeInfo[data_type].Size;
3111 for (int i = 0; i < components; i++)
3112 {
3113 PushID(int_id: i);
3114 if (i > 0)
3115 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3116 value_changed |= SliderScalar(label: "", data_type, p_data: v, p_min: v_min, p_max: v_max, format, flags);
3117 PopID();
3118 PopItemWidth();
3119 v = (void*)((char*)v + type_size);
3120 }
3121 PopID();
3122
3123 const char* label_end = FindRenderedTextEnd(text: label);
3124 if (label != label_end)
3125 {
3126 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3127 TextEx(text: label, text_end: label_end);
3128 }
3129
3130 EndGroup();
3131 return value_changed;
3132}
3133
3134bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3135{
3136 return SliderScalar(label, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3137}
3138
3139bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3140{
3141 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3142}
3143
3144bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3145{
3146 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3147}
3148
3149bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3150{
3151 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3152}
3153
3154bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3155{
3156 if (format == NULL)
3157 format = "%.0f deg";
3158 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3159 bool value_changed = SliderFloat(label, v: &v_deg, v_min: v_degrees_min, v_max: v_degrees_max, format, flags);
3160 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3161 return value_changed;
3162}
3163
3164bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3165{
3166 return SliderScalar(label, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3167}
3168
3169bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3170{
3171 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3172}
3173
3174bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3175{
3176 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3177}
3178
3179bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3180{
3181 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3182}
3183
3184bool 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)
3185{
3186 ImGuiWindow* window = GetCurrentWindow();
3187 if (window->SkipItems)
3188 return false;
3189
3190 ImGuiContext& g = *GImGui;
3191 const ImGuiStyle& style = g.Style;
3192 const ImGuiID id = window->GetID(str: label);
3193
3194 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3195 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3196 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));
3197
3198 ItemSize(bb, text_baseline_y: style.FramePadding.y);
3199 if (!ItemAdd(bb: frame_bb, id))
3200 return false;
3201
3202 // Default format string when passing NULL
3203 if (format == NULL)
3204 format = DataTypeGetInfo(data_type)->PrintFmt;
3205
3206 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
3207 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
3208 if (clicked || g.NavActivateId == id)
3209 {
3210 if (clicked)
3211 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3212 SetActiveID(id, window);
3213 SetFocusID(id, window);
3214 FocusWindow(window);
3215 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3216 }
3217
3218 // Draw frame
3219 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3220 RenderNavHighlight(bb: frame_bb, id);
3221 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
3222
3223 // Slider behavior
3224 ImRect grab_bb;
3225 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);
3226 if (value_changed)
3227 MarkItemEdited(id);
3228
3229 // Render grab
3230 if (grab_bb.Max.y > grab_bb.Min.y)
3231 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);
3232
3233 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3234 // For the vertical slider we allow centered text to overlap the frame padding
3235 char value_buf[64];
3236 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3237 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));
3238 if (label_size.x > 0.0f)
3239 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3240
3241 return value_changed;
3242}
3243
3244bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3245{
3246 return VSliderScalar(label, size, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3247}
3248
3249bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3250{
3251 return VSliderScalar(label, size, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3252}
3253
3254//-------------------------------------------------------------------------
3255// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3256//-------------------------------------------------------------------------
3257// - ImParseFormatFindStart() [Internal]
3258// - ImParseFormatFindEnd() [Internal]
3259// - ImParseFormatTrimDecorations() [Internal]
3260// - ImParseFormatSanitizeForPrinting() [Internal]
3261// - ImParseFormatSanitizeForScanning() [Internal]
3262// - ImParseFormatPrecision() [Internal]
3263// - TempInputTextScalar() [Internal]
3264// - InputScalar()
3265// - InputScalarN()
3266// - InputFloat()
3267// - InputFloat2()
3268// - InputFloat3()
3269// - InputFloat4()
3270// - InputInt()
3271// - InputInt2()
3272// - InputInt3()
3273// - InputInt4()
3274// - InputDouble()
3275//-------------------------------------------------------------------------
3276
3277// We don't use strchr() because our strings are usually very short and often start with '%'
3278const char* ImParseFormatFindStart(const char* fmt)
3279{
3280 while (char c = fmt[0])
3281 {
3282 if (c == '%' && fmt[1] != '%')
3283 return fmt;
3284 else if (c == '%')
3285 fmt++;
3286 fmt++;
3287 }
3288 return fmt;
3289}
3290
3291const char* ImParseFormatFindEnd(const char* fmt)
3292{
3293 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3294 if (fmt[0] != '%')
3295 return fmt;
3296 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3297 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3298 for (char c; (c = *fmt) != 0; fmt++)
3299 {
3300 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3301 return fmt + 1;
3302 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3303 return fmt + 1;
3304 }
3305 return fmt;
3306}
3307
3308// Extract the format out of a format string with leading or trailing decorations
3309// fmt = "blah blah" -> return ""
3310// fmt = "%.3f" -> return fmt
3311// fmt = "hello %.3f" -> return fmt + 6
3312// fmt = "%.3f hello" -> return buf written with "%.3f"
3313const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3314{
3315 const char* fmt_start = ImParseFormatFindStart(fmt);
3316 if (fmt_start[0] != '%')
3317 return "";
3318 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_start);
3319 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3320 return fmt_start;
3321 ImStrncpy(dst: buf, src: fmt_start, count: ImMin(lhs: (size_t)(fmt_end - fmt_start) + 1, rhs: buf_size));
3322 return buf;
3323}
3324
3325// Sanitize format
3326// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3327// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3328void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3329{
3330 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3331 IM_UNUSED(fmt_out_size);
3332 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3333 while (fmt_in < fmt_end)
3334 {
3335 char c = *fmt_in++;
3336 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3337 *(fmt_out++) = c;
3338 }
3339 *fmt_out = 0; // Zero-terminate
3340}
3341
3342// - 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"
3343const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3344{
3345 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3346 const char* fmt_out_begin = fmt_out;
3347 IM_UNUSED(fmt_out_size);
3348 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3349 bool has_type = false;
3350 while (fmt_in < fmt_end)
3351 {
3352 char c = *fmt_in++;
3353 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3354 continue;
3355 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3356 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3357 *(fmt_out++) = c;
3358 }
3359 *fmt_out = 0; // Zero-terminate
3360 return fmt_out_begin;
3361}
3362
3363template<typename TYPE>
3364static const char* ImAtoi(const char* src, TYPE* output)
3365{
3366 int negative = 0;
3367 if (*src == '-') { negative = 1; src++; }
3368 if (*src == '+') { src++; }
3369 TYPE v = 0;
3370 while (*src >= '0' && *src <= '9')
3371 v = (v * 10) + (*src++ - '0');
3372 *output = negative ? -v : v;
3373 return src;
3374}
3375
3376// Parse display precision back from the display format string
3377// 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.
3378int ImParseFormatPrecision(const char* fmt, int default_precision)
3379{
3380 fmt = ImParseFormatFindStart(fmt);
3381 if (fmt[0] != '%')
3382 return default_precision;
3383 fmt++;
3384 while (*fmt >= '0' && *fmt <= '9')
3385 fmt++;
3386 int precision = INT_MAX;
3387 if (*fmt == '.')
3388 {
3389 fmt = ImAtoi<int>(src: fmt + 1, output: &precision);
3390 if (precision < 0 || precision > 99)
3391 precision = default_precision;
3392 }
3393 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3394 precision = -1;
3395 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3396 precision = -1;
3397 return (precision == INT_MAX) ? default_precision : precision;
3398}
3399
3400// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3401// FIXME: Facilitate using this in variety of other situations.
3402bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3403{
3404 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3405 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3406 ImGuiContext& g = *GImGui;
3407 const bool init = (g.TempInputId != id);
3408 if (init)
3409 ClearActiveID();
3410
3411 g.CurrentWindow->DC.CursorPos = bb.Min;
3412 bool value_changed = InputTextEx(label, NULL, buf, buf_size, size_arg: bb.GetSize(), flags: flags | ImGuiInputTextFlags_MergedItem);
3413 if (init)
3414 {
3415 // First frame we started displaying the InputText widget, we expect it to take the active id.
3416 IM_ASSERT(g.ActiveId == id);
3417 g.TempInputId = g.ActiveId;
3418 }
3419 return value_changed;
3420}
3421
3422// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3423// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3424// However this may not be ideal for all uses, as some user code may break on out of bound values.
3425bool 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)
3426{
3427 // FIXME: May need to clarify display behavior if format doesn't contain %.
3428 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3429 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3430 char fmt_buf[32];
3431 char data_buf[32];
3432 format = ImParseFormatTrimDecorations(fmt: format, buf: fmt_buf, IM_ARRAYSIZE(fmt_buf));
3433 if (format[0] == 0)
3434 format = type_info->PrintFmt;
3435 DataTypeFormatString(buf: data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3436 ImStrTrimBlanks(str: data_buf);
3437
3438 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3439
3440 bool value_changed = false;
3441 if (TempInputText(bb, id, label, buf: data_buf, IM_ARRAYSIZE(data_buf), flags))
3442 {
3443 // Backup old value
3444 size_t data_type_size = type_info->Size;
3445 ImGuiDataTypeTempStorage data_backup;
3446 memcpy(dest: &data_backup, src: p_data, n: data_type_size);
3447
3448 // Apply new value (or operations) then clamp
3449 DataTypeApplyFromText(buf: data_buf, data_type, p_data, format);
3450 if (p_clamp_min || p_clamp_max)
3451 {
3452 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, arg_1: p_clamp_min, arg_2: p_clamp_max) > 0)
3453 ImSwap(a&: p_clamp_min, b&: p_clamp_max);
3454 DataTypeClamp(data_type, p_data, p_min: p_clamp_min, p_max: p_clamp_max);
3455 }
3456
3457 // Only mark as edited if new value is different
3458 value_changed = memcmp(s1: &data_backup, s2: p_data, n: data_type_size) != 0;
3459 if (value_changed)
3460 MarkItemEdited(id);
3461 }
3462 return value_changed;
3463}
3464
3465// 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.
3466// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3467bool 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)
3468{
3469 ImGuiWindow* window = GetCurrentWindow();
3470 if (window->SkipItems)
3471 return false;
3472
3473 ImGuiContext& g = *GImGui;
3474 ImGuiStyle& style = g.Style;
3475
3476 if (format == NULL)
3477 format = DataTypeGetInfo(data_type)->PrintFmt;
3478
3479 char buf[64];
3480 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3481
3482 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3483 flags |= (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3484
3485 bool value_changed = false;
3486 if (p_step == NULL)
3487 {
3488 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3489 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
3490 }
3491 else
3492 {
3493 const float button_size = GetFrameHeight();
3494
3495 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3496 PushID(str_id: label);
3497 SetNextItemWidth(ImMax(lhs: 1.0f, rhs: CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3498 if (InputText(label: "", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3499 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
3500 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3501
3502 // Step buttons
3503 const ImVec2 backup_frame_padding = style.FramePadding;
3504 style.FramePadding.x = style.FramePadding.y;
3505 ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups;
3506 if (flags & ImGuiInputTextFlags_ReadOnly)
3507 BeginDisabled();
3508 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3509 if (ButtonEx(label: "-", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3510 {
3511 DataTypeApplyOp(data_type, op: '-', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3512 value_changed = true;
3513 }
3514 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3515 if (ButtonEx(label: "+", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3516 {
3517 DataTypeApplyOp(data_type, op: '+', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3518 value_changed = true;
3519 }
3520 if (flags & ImGuiInputTextFlags_ReadOnly)
3521 EndDisabled();
3522
3523 const char* label_end = FindRenderedTextEnd(text: label);
3524 if (label != label_end)
3525 {
3526 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3527 TextEx(text: label, text_end: label_end);
3528 }
3529 style.FramePadding = backup_frame_padding;
3530
3531 PopID();
3532 EndGroup();
3533 }
3534 if (value_changed)
3535 MarkItemEdited(id: g.LastItemData.ID);
3536
3537 return value_changed;
3538}
3539
3540bool 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)
3541{
3542 ImGuiWindow* window = GetCurrentWindow();
3543 if (window->SkipItems)
3544 return false;
3545
3546 ImGuiContext& g = *GImGui;
3547 bool value_changed = false;
3548 BeginGroup();
3549 PushID(str_id: label);
3550 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3551 size_t type_size = GDataTypeInfo[data_type].Size;
3552 for (int i = 0; i < components; i++)
3553 {
3554 PushID(int_id: i);
3555 if (i > 0)
3556 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3557 value_changed |= InputScalar(label: "", data_type, p_data, p_step, p_step_fast, format, flags);
3558 PopID();
3559 PopItemWidth();
3560 p_data = (void*)((char*)p_data + type_size);
3561 }
3562 PopID();
3563
3564 const char* label_end = FindRenderedTextEnd(text: label);
3565 if (label != label_end)
3566 {
3567 SameLine(offset_from_start_x: 0.0f, spacing: g.Style.ItemInnerSpacing.x);
3568 TextEx(text: label, text_end: label_end);
3569 }
3570
3571 EndGroup();
3572 return value_changed;
3573}
3574
3575bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3576{
3577 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);
3578}
3579
3580bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3581{
3582 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, NULL, NULL, format, flags);
3583}
3584
3585bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3586{
3587 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, NULL, NULL, format, flags);
3588}
3589
3590bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3591{
3592 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, NULL, NULL, format, flags);
3593}
3594
3595bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3596{
3597 // 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.
3598 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3599 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);
3600}
3601
3602bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3603{
3604 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, NULL, NULL, format: "%d", flags);
3605}
3606
3607bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3608{
3609 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, NULL, NULL, format: "%d", flags);
3610}
3611
3612bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3613{
3614 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, NULL, NULL, format: "%d", flags);
3615}
3616
3617bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3618{
3619 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);
3620}
3621
3622//-------------------------------------------------------------------------
3623// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3624//-------------------------------------------------------------------------
3625// - InputText()
3626// - InputTextWithHint()
3627// - InputTextMultiline()
3628// - InputTextGetCharInfo() [Internal]
3629// - InputTextReindexLines() [Internal]
3630// - InputTextReindexLinesRange() [Internal]
3631// - InputTextEx() [Internal]
3632// - DebugNodeInputTextState() [Internal]
3633//-------------------------------------------------------------------------
3634
3635bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3636{
3637 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3638 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3639}
3640
3641bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3642{
3643 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: size, flags: flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3644}
3645
3646bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3647{
3648 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3649 return InputTextEx(label, hint, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3650}
3651
3652static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3653{
3654 int line_count = 0;
3655 const char* s = text_begin;
3656 while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding
3657 if (c == '\n')
3658 line_count++;
3659 s--;
3660 if (s[0] != '\n' && s[0] != '\r')
3661 line_count++;
3662 *out_text_end = s;
3663 return line_count;
3664}
3665
3666static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line)
3667{
3668 ImGuiContext& g = *ctx;
3669 ImFont* font = g.Font;
3670 const float line_height = g.FontSize;
3671 const float scale = line_height / font->FontSize;
3672
3673 ImVec2 text_size = ImVec2(0, 0);
3674 float line_width = 0.0f;
3675
3676 const ImWchar* s = text_begin;
3677 while (s < text_end)
3678 {
3679 unsigned int c = (unsigned int)(*s++);
3680 if (c == '\n')
3681 {
3682 text_size.x = ImMax(lhs: text_size.x, rhs: line_width);
3683 text_size.y += line_height;
3684 line_width = 0.0f;
3685 if (stop_on_new_line)
3686 break;
3687 continue;
3688 }
3689 if (c == '\r')
3690 continue;
3691
3692 const float char_width = font->GetCharAdvance(c: (ImWchar)c) * scale;
3693 line_width += char_width;
3694 }
3695
3696 if (text_size.x < line_width)
3697 text_size.x = line_width;
3698
3699 if (out_offset)
3700 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3701
3702 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3703 text_size.y += line_height;
3704
3705 if (remaining)
3706 *remaining = s;
3707
3708 return text_size;
3709}
3710
3711// 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)
3712namespace ImStb
3713{
3714
3715static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; }
3716static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->CurLenW); return obj->TextW[idx]; }
3717static 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.FontSize / g.Font->FontSize); }
3718static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; }
3719static ImWchar STB_TEXTEDIT_NEWLINE = '\n';
3720static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3721{
3722 const ImWchar* text = obj->TextW.Data;
3723 const ImWchar* text_remaining = NULL;
3724 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);
3725 r->x0 = 0.0f;
3726 r->x1 = size.x;
3727 r->baseline_y_delta = size.y;
3728 r->ymin = 0.0f;
3729 r->ymax = size.y;
3730 r->num_chars = (int)(text_remaining - (text + line_start_idx));
3731}
3732
3733static bool is_separator(unsigned int c)
3734{
3735 return c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r' || c=='.' || c=='!';
3736}
3737
3738static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
3739{
3740 // 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.
3741 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
3742 return 0;
3743
3744 bool prev_white = ImCharIsBlankW(c: obj->TextW[idx - 1]);
3745 bool prev_separ = is_separator(c: obj->TextW[idx - 1]);
3746 bool curr_white = ImCharIsBlankW(c: obj->TextW[idx]);
3747 bool curr_separ = is_separator(c: obj->TextW[idx]);
3748 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
3749}
3750static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
3751{
3752 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
3753 return 0;
3754
3755 bool prev_white = ImCharIsBlankW(c: obj->TextW[idx]);
3756 bool prev_separ = is_separator(c: obj->TextW[idx]);
3757 bool curr_white = ImCharIsBlankW(c: obj->TextW[idx - 1]);
3758 bool curr_separ = is_separator(c: obj->TextW[idx - 1]);
3759 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
3760}
3761static 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; }
3762static 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; }
3763static 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; }
3764static 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); }
3765#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
3766#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
3767
3768static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
3769{
3770 ImWchar* dst = obj->TextW.Data + pos;
3771
3772 // We maintain our buffer length in both UTF-8 and wchar formats
3773 obj->Edited = true;
3774 obj->CurLenA -= ImTextCountUtf8BytesFromStr(in_text: dst, in_text_end: dst + n);
3775 obj->CurLenW -= n;
3776
3777 // Offset remaining text (FIXME-OPT: Use memmove)
3778 const ImWchar* src = obj->TextW.Data + pos + n;
3779 while (ImWchar c = *src++)
3780 *dst++ = c;
3781 *dst = '\0';
3782}
3783
3784static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len)
3785{
3786 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
3787 const int text_len = obj->CurLenW;
3788 IM_ASSERT(pos <= text_len);
3789
3790 const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(in_text: new_text, in_text_end: new_text + new_text_len);
3791 if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA))
3792 return false;
3793
3794 // Grow internal buffer if needed
3795 if (new_text_len + text_len + 1 > obj->TextW.Size)
3796 {
3797 if (!is_resizable)
3798 return false;
3799 IM_ASSERT(text_len < obj->TextW.Size);
3800 obj->TextW.resize(new_size: text_len + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1);
3801 }
3802
3803 ImWchar* text = obj->TextW.Data;
3804 if (pos != text_len)
3805 memmove(dest: text + pos + new_text_len, src: text + pos, n: (size_t)(text_len - pos) * sizeof(ImWchar));
3806 memcpy(dest: text + pos, src: new_text, n: (size_t)new_text_len * sizeof(ImWchar));
3807
3808 obj->Edited = true;
3809 obj->CurLenW += new_text_len;
3810 obj->CurLenA += new_text_len_utf8;
3811 obj->TextW[obj->CurLenW] = '\0';
3812
3813 return true;
3814}
3815
3816// 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)
3817#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
3818#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
3819#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
3820#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
3821#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
3822#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
3823#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
3824#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
3825#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
3826#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
3827#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
3828#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
3829#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
3830#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
3831#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
3832#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
3833#define STB_TEXTEDIT_K_SHIFT 0x400000
3834
3835#define IMSTB_TEXTEDIT_IMPLEMENTATION
3836#define IMSTB_TEXTEDIT_memmove memmove
3837#include "imstb_textedit.h"
3838
3839// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
3840// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
3841static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
3842{
3843 stb_text_makeundo_replace(str, state, where: 0, old_length: str->CurLenW, new_length: text_len);
3844 ImStb::STB_TEXTEDIT_DELETECHARS(obj: str, pos: 0, n: str->CurLenW);
3845 state->cursor = state->select_start = state->select_end = 0;
3846 if (text_len <= 0)
3847 return;
3848 if (ImStb::STB_TEXTEDIT_INSERTCHARS(obj: str, pos: 0, new_text: text, new_text_len: text_len))
3849 {
3850 state->cursor = state->select_start = state->select_end = text_len;
3851 state->has_preferred_x = 0;
3852 return;
3853 }
3854 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
3855}
3856
3857} // namespace ImStb
3858
3859void ImGuiInputTextState::OnKeyPressed(int key)
3860{
3861 stb_textedit_key(str: this, state: &Stb, key);
3862 CursorFollow = true;
3863 CursorAnimReset();
3864}
3865
3866ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
3867{
3868 memset(s: this, c: 0, n: sizeof(*this));
3869}
3870
3871// Public API to manipulate UTF-8 text
3872// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar)
3873// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
3874void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
3875{
3876 IM_ASSERT(pos + bytes_count <= BufTextLen);
3877 char* dst = Buf + pos;
3878 const char* src = Buf + pos + bytes_count;
3879 while (char c = *src++)
3880 *dst++ = c;
3881 *dst = '\0';
3882
3883 if (CursorPos >= pos + bytes_count)
3884 CursorPos -= bytes_count;
3885 else if (CursorPos >= pos)
3886 CursorPos = pos;
3887 SelectionStart = SelectionEnd = CursorPos;
3888 BufDirty = true;
3889 BufTextLen -= bytes_count;
3890}
3891
3892void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
3893{
3894 // Accept null ranges
3895 if (new_text == new_text_end)
3896 return;
3897
3898 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
3899 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(s: new_text);
3900 if (new_text_len + BufTextLen >= BufSize)
3901 {
3902 if (!is_resizable)
3903 return;
3904
3905 // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!)
3906 ImGuiContext& g = *Ctx;
3907 ImGuiInputTextState* edit_state = &g.InputTextState;
3908 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
3909 IM_ASSERT(Buf == edit_state->TextA.Data);
3910 int new_buf_size = BufTextLen + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1;
3911 edit_state->TextA.reserve(new_capacity: new_buf_size + 1);
3912 Buf = edit_state->TextA.Data;
3913 BufSize = edit_state->BufCapacityA = new_buf_size;
3914 }
3915
3916 if (BufTextLen != pos)
3917 memmove(dest: Buf + pos + new_text_len, src: Buf + pos, n: (size_t)(BufTextLen - pos));
3918 memcpy(dest: Buf + pos, src: new_text, n: (size_t)new_text_len * sizeof(char));
3919 Buf[BufTextLen + new_text_len] = '\0';
3920
3921 if (CursorPos >= pos)
3922 CursorPos += new_text_len;
3923 SelectionStart = SelectionEnd = CursorPos;
3924 BufDirty = true;
3925 BufTextLen += new_text_len;
3926}
3927
3928// Return false to discard a character.
3929static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
3930{
3931 unsigned int c = *p_char;
3932
3933 // Filter non-printable (NB: isprint is unreliable! see #2467)
3934 bool apply_named_filters = true;
3935 if (c < 0x20)
3936 {
3937 bool pass = false;
3938 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)
3939 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
3940 if (!pass)
3941 return false;
3942 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
3943 }
3944
3945 if (input_source_is_clipboard == false)
3946 {
3947 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
3948 if (c == 127)
3949 return false;
3950
3951 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
3952 if (c >= 0xE000 && c <= 0xF8FF)
3953 return false;
3954 }
3955
3956 // Filter Unicode ranges we are not handling in this build
3957 if (c > IM_UNICODE_CODEPOINT_MAX)
3958 return false;
3959
3960 // Generic named filters
3961 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
3962 {
3963 // 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 '.'.
3964 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
3965 // 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.
3966 // Change the default decimal_point with:
3967 // ImGui::GetIO()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point;
3968 // 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.
3969 ImGuiContext& g = *ctx;
3970 const unsigned c_decimal_point = (unsigned int)g.IO.PlatformLocaleDecimalPoint;
3971 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
3972 if (c == '.' || c == ',')
3973 c = c_decimal_point;
3974
3975 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
3976 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
3977 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
3978 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
3979 if (c >= 0xFF01 && c <= 0xFF5E)
3980 c = c - 0xFF01 + 0x21;
3981
3982 // Allow 0-9 . - + * /
3983 if (flags & ImGuiInputTextFlags_CharsDecimal)
3984 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
3985 return false;
3986
3987 // Allow 0-9 . - + * / e E
3988 if (flags & ImGuiInputTextFlags_CharsScientific)
3989 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
3990 return false;
3991
3992 // Allow 0-9 a-F A-F
3993 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
3994 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
3995 return false;
3996
3997 // Turn a-z into A-Z
3998 if (flags & ImGuiInputTextFlags_CharsUppercase)
3999 if (c >= 'a' && c <= 'z')
4000 c += (unsigned int)('A' - 'a');
4001
4002 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4003 if (ImCharIsBlankW(c))
4004 return false;
4005
4006 *p_char = c;
4007 }
4008
4009 // Custom callback filter
4010 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4011 {
4012 ImGuiContext& g = *GImGui;
4013 ImGuiInputTextCallbackData callback_data;
4014 callback_data.Ctx = &g;
4015 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4016 callback_data.EventChar = (ImWchar)c;
4017 callback_data.Flags = flags;
4018 callback_data.UserData = user_data;
4019 if (callback(&callback_data) != 0)
4020 return false;
4021 *p_char = callback_data.EventChar;
4022 if (!callback_data.EventChar)
4023 return false;
4024 }
4025
4026 return true;
4027}
4028
4029// Find the shortest single replacement we can make to get the new text from the old text.
4030// Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end.
4031// 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.
4032static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
4033{
4034 ImGuiContext& g = *GImGui;
4035 const ImWchar* old_buf = state->TextW.Data;
4036 const int old_length = state->CurLenW;
4037 const int new_length = ImTextCountCharsFromUtf8(in_text: new_buf_a, in_text_end: new_buf_a + new_length_a);
4038 g.TempBuffer.reserve_discard(new_capacity: (new_length + 1) * sizeof(ImWchar));
4039 ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data;
4040 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);
4041
4042 const int shorter_length = ImMin(lhs: old_length, rhs: new_length);
4043 int first_diff;
4044 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4045 if (old_buf[first_diff] != new_buf[first_diff])
4046 break;
4047 if (first_diff == old_length && first_diff == new_length)
4048 return;
4049
4050 int old_last_diff = old_length - 1;
4051 int new_last_diff = new_length - 1;
4052 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4053 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4054 break;
4055
4056 const int insert_len = new_last_diff - first_diff + 1;
4057 const int delete_len = old_last_diff - first_diff + 1;
4058 if (insert_len > 0 || delete_len > 0)
4059 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(state: &state->Stb.undostate, pos: first_diff, insert_len: delete_len, delete_len: insert_len))
4060 for (int i = 0; i < delete_len; i++)
4061 p[i] = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: first_diff + i);
4062}
4063
4064// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4065// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4066// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4067// but that more likely be attractive when we do have _NoLiveEdit flag available.
4068void ImGui::InputTextDeactivateHook(ImGuiID id)
4069{
4070 ImGuiContext& g = *GImGui;
4071 ImGuiInputTextState* state = &g.InputTextState;
4072 if (id == 0 || state->ID != id)
4073 return;
4074 g.InputTextDeactivatedState.ID = state->ID;
4075 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4076 {
4077 g.InputTextDeactivatedState.TextA.resize(new_size: 0); // In theory this data won't be used, but clear to be neat.
4078 }
4079 else
4080 {
4081 IM_ASSERT(state->TextA.Data != 0);
4082 g.InputTextDeactivatedState.TextA.resize(new_size: state->CurLenA + 1);
4083 memcpy(dest: g.InputTextDeactivatedState.TextA.Data, src: state->TextA.Data, n: state->CurLenA + 1);
4084 }
4085}
4086
4087// Edit a string of text
4088// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4089// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4090// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4091// - 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.
4092// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4093// (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
4094// 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)
4095bool 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)
4096{
4097 ImGuiWindow* window = GetCurrentWindow();
4098 if (window->SkipItems)
4099 return false;
4100
4101 IM_ASSERT(buf != NULL && buf_size >= 0);
4102 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4103 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4104
4105 ImGuiContext& g = *GImGui;
4106 ImGuiIO& io = g.IO;
4107 const ImGuiStyle& style = g.Style;
4108
4109 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4110 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4111
4112 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4113 BeginGroup();
4114 const ImGuiID id = window->GetID(str: label);
4115 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
4116 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
4117 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4118
4119 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4120 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4121
4122 ImGuiWindow* draw_window = window;
4123 ImVec2 inner_size = frame_size;
4124 ImGuiLastItemData item_data_backup;
4125 if (is_multiline)
4126 {
4127 ImVec2 backup_pos = window->DC.CursorPos;
4128 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4129 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4130 {
4131 EndGroup();
4132 return false;
4133 }
4134 item_data_backup = g.LastItemData;
4135 window->DC.CursorPos = backup_pos;
4136
4137 // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4138 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4139 g.NavActivateId = 0;
4140
4141 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4142 const ImGuiID backup_activate_id = g.NavActivateId;
4143 if (g.ActiveId == id) // Prevent reactivation
4144 g.NavActivateId = 0;
4145
4146 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4147 PushStyleColor(idx: ImGuiCol_ChildBg, col: style.Colors[ImGuiCol_FrameBg]);
4148 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.FrameRounding);
4149 PushStyleVar(idx: ImGuiStyleVar_ChildBorderSize, val: style.FrameBorderSize);
4150 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4151 bool child_visible = BeginChildEx(name: label, id, size_arg: frame_bb.GetSize(), child_flags: true, window_flags: ImGuiWindowFlags_NoMove);
4152 g.NavActivateId = backup_activate_id;
4153 PopStyleVar(count: 3);
4154 PopStyleColor();
4155 if (!child_visible)
4156 {
4157 EndChild();
4158 EndGroup();
4159 return false;
4160 }
4161 draw_window = g.CurrentWindow; // Child window
4162 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.
4163 draw_window->DC.CursorPos += style.FramePadding;
4164 inner_size.x -= draw_window->ScrollbarSizes.x;
4165 }
4166 else
4167 {
4168 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4169 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4170 if (!(flags & ImGuiInputTextFlags_MergedItem))
4171 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4172 return false;
4173 }
4174 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
4175 if (hovered)
4176 g.MouseCursor = ImGuiMouseCursor_TextInput;
4177
4178 // We are only allowed to access the state if we are already the active widget.
4179 ImGuiInputTextState* state = GetInputTextState(id);
4180
4181 if (g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly)
4182 flags |= ImGuiInputTextFlags_ReadOnly;
4183 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4184 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4185 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4186 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4187 if (is_resizable)
4188 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4189
4190 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4191
4192 const bool user_clicked = hovered && io.MouseClicked[0];
4193 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4194 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4195 bool clear_active_id = false;
4196 bool select_all = false;
4197
4198 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4199
4200 const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf);
4201 const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline); // state != NULL means its our state.
4202 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4203 const bool init_state = (init_make_active || user_scroll_active);
4204 if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf)
4205 {
4206 // Access state even if we don't own it yet.
4207 state = &g.InputTextState;
4208 state->CursorAnimReset();
4209 state->ReloadUserBuf = false;
4210
4211 // 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)
4212 InputTextDeactivateHook(id: state->ID);
4213
4214 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4215 const int buf_len = (int)strlen(s: buf);
4216 if (!init_reload_from_user_buf)
4217 {
4218 // Take a copy of the initial buffer value.
4219 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.
4220 memcpy(dest: state->InitialTextA.Data, src: buf, n: buf_len + 1);
4221 }
4222
4223 // Preserve cursor position and undo/redo stack if we come back to same widget
4224 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4225 bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf);
4226 if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(s1: state->TextA.Data, s2: buf, n: buf_len) != 0)))
4227 recycle_state = false;
4228
4229 // Start edition
4230 const char* buf_end = NULL;
4231 state->ID = id;
4232 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.
4233 state->TextA.resize(new_size: 0);
4234 state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then)
4235 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: buf_size, in_text: buf, NULL, in_remaining: &buf_end);
4236 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.
4237
4238 if (recycle_state)
4239 {
4240 // Recycle existing cursor/selection/undo stack but clamp position
4241 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4242 state->CursorClamp();
4243 }
4244 else
4245 {
4246 state->ScrollX = 0.0f;
4247 stb_textedit_initialize_state(state: &state->Stb, is_single_line: !is_multiline);
4248 }
4249
4250 if (init_reload_from_user_buf)
4251 {
4252 state->Stb.select_start = state->ReloadSelectionStart;
4253 state->Stb.cursor = state->Stb.select_end = state->ReloadSelectionEnd;
4254 state->CursorClamp();
4255 }
4256 else if (!is_multiline)
4257 {
4258 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4259 select_all = true;
4260 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4261 select_all = true;
4262 if (user_clicked && io.KeyCtrl)
4263 select_all = true;
4264 }
4265
4266 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4267 state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4268 }
4269
4270 const bool is_osx = io.ConfigMacOSXBehaviors;
4271 if (g.ActiveId != id && init_make_active)
4272 {
4273 IM_ASSERT(state && state->ID == id);
4274 SetActiveID(id, window);
4275 SetFocusID(id, window);
4276 FocusWindow(window);
4277 }
4278 if (g.ActiveId == id)
4279 {
4280 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4281 if (user_clicked)
4282 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
4283 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4284 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4285 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4286 SetKeyOwner(key: ImGuiKey_Enter, owner_id: id);
4287 SetKeyOwner(key: ImGuiKey_KeypadEnter, owner_id: id);
4288 SetKeyOwner(key: ImGuiKey_Home, owner_id: id);
4289 SetKeyOwner(key: ImGuiKey_End, owner_id: id);
4290 if (is_multiline)
4291 {
4292 SetKeyOwner(key: ImGuiKey_PageUp, owner_id: id);
4293 SetKeyOwner(key: ImGuiKey_PageDown, owner_id: id);
4294 }
4295 if (is_osx)
4296 SetKeyOwner(key: ImGuiMod_Alt, owner_id: id);
4297 }
4298
4299 // 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)
4300 if (g.ActiveId == id && state == NULL)
4301 ClearActiveID();
4302
4303 // Release focus when we click outside
4304 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4305 clear_active_id = true;
4306
4307 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4308 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4309 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4310 bool value_changed = false;
4311 bool validated = false;
4312
4313 // When read-only we always use the live data passed to the function
4314 // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :(
4315 if (is_readonly && state != NULL && (render_cursor || render_selection))
4316 {
4317 const char* buf_end = NULL;
4318 state->TextW.resize(new_size: buf_size + 1);
4319 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: buf, NULL, in_remaining: &buf_end);
4320 state->CurLenA = (int)(buf_end - buf);
4321 state->CursorClamp();
4322 render_selection &= state->HasSelection();
4323 }
4324
4325 // Select the buffer to render.
4326 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid;
4327 const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4328
4329 // Password pushes a temporary font with only a fallback glyph
4330 if (is_password && !is_displaying_hint)
4331 {
4332 const ImFontGlyph* glyph = g.Font->FindGlyph(c: '*');
4333 ImFont* password_font = &g.InputTextPasswordFont;
4334 password_font->FontSize = g.Font->FontSize;
4335 password_font->Scale = g.Font->Scale;
4336 password_font->Ascent = g.Font->Ascent;
4337 password_font->Descent = g.Font->Descent;
4338 password_font->ContainerAtlas = g.Font->ContainerAtlas;
4339 password_font->FallbackGlyph = glyph;
4340 password_font->FallbackAdvanceX = glyph->AdvanceX;
4341 IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
4342 PushFont(font: password_font);
4343 }
4344
4345 // Process mouse inputs and character inputs
4346 int backup_current_text_length = 0;
4347 if (g.ActiveId == id)
4348 {
4349 IM_ASSERT(state != NULL);
4350 backup_current_text_length = state->CurLenA;
4351 state->Edited = false;
4352 state->BufCapacityA = buf_size;
4353 state->Flags = flags;
4354
4355 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4356 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4357 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4358
4359 // Edit in progress
4360 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX;
4361 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4362
4363 if (select_all)
4364 {
4365 state->SelectAll();
4366 state->SelectedAllMouseLock = true;
4367 }
4368 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4369 {
4370 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4371 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4372 if ((multiclick_count % 2) == 0)
4373 {
4374 // Double-click: Select word
4375 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4376 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4377 const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor - 1) == '\n';
4378 if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol)
4379 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4380 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4381 if (!STB_TEXT_HAS_SELECTION(&state->Stb))
4382 ImStb::stb_textedit_prep_selection_at_cursor(state: &state->Stb);
4383 state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj: state, idx: state->Stb.cursor);
4384 state->Stb.select_end = state->Stb.cursor;
4385 ImStb::stb_textedit_clamp(str: state, state: &state->Stb);
4386 }
4387 else
4388 {
4389 // Triple-click: Select line
4390 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor) == '\n';
4391 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4392 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4393 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4394 if (!is_eol && is_multiline)
4395 {
4396 ImSwap(a&: state->Stb.select_start, b&: state->Stb.select_end);
4397 state->Stb.cursor = state->Stb.select_end;
4398 }
4399 state->CursorFollow = false;
4400 }
4401 state->CursorAnimReset();
4402 }
4403 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4404 {
4405 if (hovered)
4406 {
4407 if (io.KeyShift)
4408 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4409 else
4410 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4411 state->CursorAnimReset();
4412 }
4413 }
4414 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4415 {
4416 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4417 state->CursorAnimReset();
4418 state->CursorFollow = true;
4419 }
4420 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4421 state->SelectedAllMouseLock = false;
4422
4423 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4424 // (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)
4425 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4426 {
4427 if (Shortcut(key_chord: ImGuiKey_Tab, owner_id: id, flags: ImGuiInputFlags_Repeat))
4428 {
4429 unsigned int c = '\t'; // Insert TAB
4430 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4431 state->OnKeyPressed(key: (int)c);
4432 }
4433 // FIXME: Implement Shift+Tab
4434 /*
4435 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, id, ImGuiInputFlags_Repeat))
4436 {
4437 }
4438 */
4439 }
4440
4441 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4442 // 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.
4443 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper);
4444 if (io.InputQueueCharacters.Size > 0)
4445 {
4446 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4447 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4448 {
4449 // Insert character if they pass filtering
4450 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4451 if (c == '\t') // Skip Tab, see above.
4452 continue;
4453 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4454 state->OnKeyPressed(key: (int)c);
4455 }
4456
4457 // Consume characters
4458 io.InputQueueCharacters.resize(new_size: 0);
4459 }
4460 }
4461
4462 // Process other shortcuts/key-presses
4463 bool revert_edit = false;
4464 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4465 {
4466 IM_ASSERT(state != NULL);
4467
4468 const int row_count_per_page = ImMax(lhs: (int)((inner_size.y - style.FramePadding.y) / g.FontSize), rhs: 1);
4469 state->Stb.row_count_per_page = row_count_per_page;
4470
4471 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4472 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4473 const bool is_startend_key_down = is_osx && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
4474
4475 // 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)
4476 // Otherwise we could simply assume that we own the keys as we are active.
4477 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4478 const bool is_cut = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_X, owner_id: id, flags: f_repeat) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Delete, owner_id: id, flags: f_repeat)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
4479 const bool is_copy = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_C, owner_id: id) || Shortcut(key_chord: ImGuiMod_Ctrl | ImGuiKey_Insert, owner_id: id)) && !is_password && (!is_multiline || state->HasSelection());
4480 const bool is_paste = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_V, owner_id: id, flags: f_repeat) || Shortcut(key_chord: ImGuiMod_Shift | ImGuiKey_Insert, owner_id: id, flags: f_repeat)) && !is_readonly;
4481 const bool is_undo = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_Z, owner_id: id, flags: f_repeat)) && !is_readonly && is_undoable;
4482 const bool is_redo = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_Y, owner_id: id, flags: f_repeat) || (is_osx && Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiMod_Shift | ImGuiKey_Z, owner_id: id, flags: f_repeat))) && !is_readonly && is_undoable;
4483 const bool is_select_all = Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_A, owner_id: id);
4484
4485 // 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.
4486 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4487 const bool is_enter_pressed = IsKeyPressed(key: ImGuiKey_Enter, repeat: true) || IsKeyPressed(key: ImGuiKey_KeypadEnter, repeat: true);
4488 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, repeat: false) || IsKeyPressed(ImGuiKey_NavGamepadInput, repeat: false));
4489 const bool is_cancel = Shortcut(key_chord: ImGuiKey_Escape, owner_id: id, flags: f_repeat) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, owner_id: id, flags: f_repeat));
4490
4491 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4492 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); }
4493 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); }
4494 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); }
4495 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); }
4496 else if (IsKeyPressed(key: ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4497 else if (IsKeyPressed(key: ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4498 else if (IsKeyPressed(key: ImGuiKey_Home)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4499 else if (IsKeyPressed(key: ImGuiKey_End)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4500 else if (IsKeyPressed(key: ImGuiKey_Delete) && !is_readonly && !is_cut)
4501 {
4502 if (!state->HasSelection())
4503 {
4504 // 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)
4505 if (is_wordmove_key_down)
4506 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4507 }
4508 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4509 }
4510 else if (IsKeyPressed(key: ImGuiKey_Backspace) && !is_readonly)
4511 {
4512 if (!state->HasSelection())
4513 {
4514 if (is_wordmove_key_down)
4515 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4516 else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl)
4517 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4518 }
4519 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4520 }
4521 else if (is_enter_pressed || is_gamepad_validate)
4522 {
4523 // Determine if we turn Enter into a \n character
4524 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4525 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4526 {
4527 validated = true;
4528 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4529 state->SelectAll(); // No need to scroll
4530 else
4531 clear_active_id = true;
4532 }
4533 else if (!is_readonly)
4534 {
4535 unsigned int c = '\n'; // Insert new line
4536 if (InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data))
4537 state->OnKeyPressed(key: (int)c);
4538 }
4539 }
4540 else if (is_cancel)
4541 {
4542 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4543 {
4544 if (buf[0] != 0)
4545 {
4546 revert_edit = true;
4547 }
4548 else
4549 {
4550 render_cursor = render_selection = false;
4551 clear_active_id = true;
4552 }
4553 }
4554 else
4555 {
4556 clear_active_id = revert_edit = true;
4557 render_cursor = render_selection = false;
4558 }
4559 }
4560 else if (is_undo || is_redo)
4561 {
4562 state->OnKeyPressed(key: is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4563 state->ClearSelection();
4564 }
4565 else if (is_select_all)
4566 {
4567 state->SelectAll();
4568 state->CursorFollow = true;
4569 }
4570 else if (is_cut || is_copy)
4571 {
4572 // Cut, Copy
4573 if (io.SetClipboardTextFn)
4574 {
4575 const int ib = state->HasSelection() ? ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : 0;
4576 const int ie = state->HasSelection() ? ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : state->CurLenW;
4577 const int clipboard_data_len = ImTextCountUtf8BytesFromStr(in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie) + 1;
4578 char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char));
4579 ImTextStrToUtf8(out_buf: clipboard_data, out_buf_size: clipboard_data_len, in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie);
4580 SetClipboardText(clipboard_data);
4581 MemFree(ptr: clipboard_data);
4582 }
4583 if (is_cut)
4584 {
4585 if (!state->HasSelection())
4586 state->SelectAll();
4587 state->CursorFollow = true;
4588 stb_textedit_cut(str: state, state: &state->Stb);
4589 }
4590 }
4591 else if (is_paste)
4592 {
4593 if (const char* clipboard = GetClipboardText())
4594 {
4595 // Filter pasted buffer
4596 const int clipboard_len = (int)strlen(s: clipboard);
4597 ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
4598 int clipboard_filtered_len = 0;
4599 for (const char* s = clipboard; *s != 0; )
4600 {
4601 unsigned int c;
4602 s += ImTextCharFromUtf8(out_char: &c, in_text: s, NULL);
4603 if (!InputTextFilterCharacter(ctx: &g, p_char: &c, flags, callback, user_data: callback_user_data, input_source_is_clipboard: true))
4604 continue;
4605 clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
4606 }
4607 clipboard_filtered[clipboard_filtered_len] = 0;
4608 if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
4609 {
4610 stb_textedit_paste(str: state, state: &state->Stb, ctext: clipboard_filtered, len: clipboard_filtered_len);
4611 state->CursorFollow = true;
4612 }
4613 MemFree(ptr: clipboard_filtered);
4614 }
4615 }
4616
4617 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4618 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4619 }
4620
4621 // Process callbacks and apply result back to user's buffer.
4622 const char* apply_new_text = NULL;
4623 int apply_new_text_length = 0;
4624 if (g.ActiveId == id)
4625 {
4626 IM_ASSERT(state != NULL);
4627 if (revert_edit && !is_readonly)
4628 {
4629 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4630 {
4631 // Clear input
4632 IM_ASSERT(buf[0] != 0);
4633 apply_new_text = "";
4634 apply_new_text_length = 0;
4635 value_changed = true;
4636 IMSTB_TEXTEDIT_CHARTYPE empty_string;
4637 stb_textedit_replace(str: state, state: &state->Stb, text: &empty_string, text_len: 0);
4638 }
4639 else if (strcmp(s1: buf, s2: state->InitialTextA.Data) != 0)
4640 {
4641 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
4642 // Push records into the undo stack so we can CTRL+Z the revert operation itself
4643 apply_new_text = state->InitialTextA.Data;
4644 apply_new_text_length = state->InitialTextA.Size - 1;
4645 value_changed = true;
4646 ImVector<ImWchar> w_text;
4647 if (apply_new_text_length > 0)
4648 {
4649 w_text.resize(new_size: ImTextCountCharsFromUtf8(in_text: apply_new_text, in_text_end: apply_new_text + apply_new_text_length) + 1);
4650 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);
4651 }
4652 stb_textedit_replace(str: state, state: &state->Stb, text: w_text.Data, text_len: (apply_new_text_length > 0) ? (w_text.Size - 1) : 0);
4653 }
4654 }
4655
4656 // Apply ASCII value
4657 if (!is_readonly)
4658 {
4659 state->TextAIsValid = true;
4660 state->TextA.resize(new_size: state->TextW.Size * 4 + 1);
4661 ImTextStrToUtf8(out_buf: state->TextA.Data, out_buf_size: state->TextA.Size, in_text: state->TextW.Data, NULL);
4662 }
4663
4664 // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer
4665 // before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
4666 // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
4667 // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage
4668 // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
4669 // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
4670 const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
4671 if (apply_edit_back_to_user_buffer)
4672 {
4673 // Apply new value immediately - copy modified buffer back
4674 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
4675 // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
4676 // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
4677
4678 // User callback
4679 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
4680 {
4681 IM_ASSERT(callback != NULL);
4682
4683 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
4684 ImGuiInputTextFlags event_flag = 0;
4685 ImGuiKey event_key = ImGuiKey_None;
4686 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(key_chord: ImGuiKey_Tab, owner_id: id))
4687 {
4688 event_flag = ImGuiInputTextFlags_CallbackCompletion;
4689 event_key = ImGuiKey_Tab;
4690 }
4691 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_UpArrow))
4692 {
4693 event_flag = ImGuiInputTextFlags_CallbackHistory;
4694 event_key = ImGuiKey_UpArrow;
4695 }
4696 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_DownArrow))
4697 {
4698 event_flag = ImGuiInputTextFlags_CallbackHistory;
4699 event_key = ImGuiKey_DownArrow;
4700 }
4701 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
4702 {
4703 event_flag = ImGuiInputTextFlags_CallbackEdit;
4704 }
4705 else if (flags & ImGuiInputTextFlags_CallbackAlways)
4706 {
4707 event_flag = ImGuiInputTextFlags_CallbackAlways;
4708 }
4709
4710 if (event_flag)
4711 {
4712 ImGuiInputTextCallbackData callback_data;
4713 callback_data.Ctx = &g;
4714 callback_data.EventFlag = event_flag;
4715 callback_data.Flags = flags;
4716 callback_data.UserData = callback_user_data;
4717
4718 char* callback_buf = is_readonly ? buf : state->TextA.Data;
4719 callback_data.EventKey = event_key;
4720 callback_data.Buf = callback_buf;
4721 callback_data.BufTextLen = state->CurLenA;
4722 callback_data.BufSize = state->BufCapacityA;
4723 callback_data.BufDirty = false;
4724
4725 // 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)
4726 ImWchar* text = state->TextW.Data;
4727 const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.cursor);
4728 const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_start);
4729 const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_end);
4730
4731 // Call user code
4732 callback(&callback_data);
4733
4734 // Read back what user may have modified
4735 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
4736 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
4737 IM_ASSERT(callback_data.BufSize == state->BufCapacityA);
4738 IM_ASSERT(callback_data.Flags == flags);
4739 const bool buf_dirty = callback_data.BufDirty;
4740 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; }
4741 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); }
4742 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); }
4743 if (buf_dirty)
4744 {
4745 IM_ASSERT(!is_readonly);
4746 IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
4747 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() ?
4748 if (callback_data.BufTextLen > backup_current_text_length && is_resizable)
4749 state->TextW.resize(new_size: state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize
4750 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: callback_data.Buf, NULL);
4751 state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
4752 state->CursorAnimReset();
4753 }
4754 }
4755 }
4756
4757 // Will copy result string if modified
4758 if (!is_readonly && strcmp(s1: state->TextA.Data, s2: buf) != 0)
4759 {
4760 apply_new_text = state->TextA.Data;
4761 apply_new_text_length = state->CurLenA;
4762 value_changed = true;
4763 }
4764 }
4765 }
4766
4767 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
4768 if (g.InputTextDeactivatedState.ID == id)
4769 {
4770 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(s1: g.InputTextDeactivatedState.TextA.Data, s2: buf) != 0)
4771 {
4772 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
4773 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
4774 value_changed = true;
4775 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
4776 }
4777 g.InputTextDeactivatedState.ID = 0;
4778 }
4779
4780 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
4781 if (apply_new_text != NULL)
4782 {
4783 // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
4784 // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
4785 // without any storage on user's side.
4786 IM_ASSERT(apply_new_text_length >= 0);
4787 if (is_resizable)
4788 {
4789 ImGuiInputTextCallbackData callback_data;
4790 callback_data.Ctx = &g;
4791 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
4792 callback_data.Flags = flags;
4793 callback_data.Buf = buf;
4794 callback_data.BufTextLen = apply_new_text_length;
4795 callback_data.BufSize = ImMax(lhs: buf_size, rhs: apply_new_text_length + 1);
4796 callback_data.UserData = callback_user_data;
4797 callback(&callback_data);
4798 buf = callback_data.Buf;
4799 buf_size = callback_data.BufSize;
4800 apply_new_text_length = ImMin(lhs: callback_data.BufTextLen, rhs: buf_size - 1);
4801 IM_ASSERT(apply_new_text_length <= buf_size);
4802 }
4803 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
4804
4805 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
4806 ImStrncpy(dst: buf, src: apply_new_text, count: ImMin(lhs: apply_new_text_length + 1, rhs: buf_size));
4807 }
4808
4809 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
4810 // Otherwise request text input ahead for next frame.
4811 if (g.ActiveId == id && clear_active_id)
4812 ClearActiveID();
4813 else if (g.ActiveId == id)
4814 g.WantTextInputNextFrame = 1;
4815
4816 // Render frame
4817 if (!is_multiline)
4818 {
4819 RenderNavHighlight(bb: frame_bb, id);
4820 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
4821 }
4822
4823 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
4824 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
4825 ImVec2 text_size(0.0f, 0.0f);
4826
4827 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
4828 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
4829 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
4830 const int buf_display_max_length = 2 * 1024 * 1024;
4831 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
4832 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
4833 if (is_displaying_hint)
4834 {
4835 buf_display = hint;
4836 buf_display_end = hint + strlen(s: hint);
4837 }
4838
4839 // Render text. We currently only render selection when the widget is active or while scrolling.
4840 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
4841 if (render_cursor || render_selection)
4842 {
4843 IM_ASSERT(state != NULL);
4844 if (!is_displaying_hint)
4845 buf_display_end = buf_display + state->CurLenA;
4846
4847 // Render text (with cursor and selection)
4848 // This is going to be messy. We need to:
4849 // - Display the text (this alone can be more easily clipped)
4850 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
4851 // - Measure text height (for scrollbar)
4852 // 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)
4853 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
4854 const ImWchar* text_begin = state->TextW.Data;
4855 ImVec2 cursor_offset, select_start_offset;
4856
4857 {
4858 // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions.
4859 const ImWchar* searches_input_ptr[2] = { NULL, NULL };
4860 int searches_result_line_no[2] = { -1000, -1000 };
4861 int searches_remaining = 0;
4862 if (render_cursor)
4863 {
4864 searches_input_ptr[0] = text_begin + state->Stb.cursor;
4865 searches_result_line_no[0] = -1;
4866 searches_remaining++;
4867 }
4868 if (render_selection)
4869 {
4870 searches_input_ptr[1] = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4871 searches_result_line_no[1] = -1;
4872 searches_remaining++;
4873 }
4874
4875 // Iterate all lines to find our line numbers
4876 // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter.
4877 searches_remaining += is_multiline ? 1 : 0;
4878 int line_count = 0;
4879 //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
4880 for (const ImWchar* s = text_begin; *s != 0; s++)
4881 if (*s == '\n')
4882 {
4883 line_count++;
4884 if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; }
4885 if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; }
4886 }
4887 line_count++;
4888 if (searches_result_line_no[0] == -1)
4889 searches_result_line_no[0] = line_count;
4890 if (searches_result_line_no[1] == -1)
4891 searches_result_line_no[1] = line_count;
4892
4893 // Calculate 2d position by finding the beginning of the line and measuring distance
4894 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;
4895 cursor_offset.y = searches_result_line_no[0] * g.FontSize;
4896 if (searches_result_line_no[1] >= 0)
4897 {
4898 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;
4899 select_start_offset.y = searches_result_line_no[1] * g.FontSize;
4900 }
4901
4902 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
4903 if (is_multiline)
4904 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
4905 }
4906
4907 // Scroll
4908 if (render_cursor && state->CursorFollow)
4909 {
4910 // Horizontal scroll in chunks of quarter width
4911 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
4912 {
4913 const float scroll_increment_x = inner_size.x * 0.25f;
4914 const float visible_width = inner_size.x - style.FramePadding.x;
4915 if (cursor_offset.x < state->ScrollX)
4916 state->ScrollX = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
4917 else if (cursor_offset.x - visible_width >= state->ScrollX)
4918 state->ScrollX = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
4919 }
4920 else
4921 {
4922 state->ScrollX = 0.0f;
4923 }
4924
4925 // Vertical scroll
4926 if (is_multiline)
4927 {
4928 // Test if cursor is vertically visible
4929 if (cursor_offset.y - g.FontSize < scroll_y)
4930 scroll_y = ImMax(lhs: 0.0f, rhs: cursor_offset.y - g.FontSize);
4931 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
4932 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
4933 const float scroll_max_y = ImMax(lhs: (text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, rhs: 0.0f);
4934 scroll_y = ImClamp(v: scroll_y, mn: 0.0f, mx: scroll_max_y);
4935 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
4936 draw_window->Scroll.y = scroll_y;
4937 }
4938
4939 state->CursorFollow = false;
4940 }
4941
4942 // Draw selection
4943 const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f);
4944 if (render_selection)
4945 {
4946 const ImWchar* text_selected_begin = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4947 const ImWchar* text_selected_end = text_begin + ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4948
4949 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.
4950 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.
4951 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
4952 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
4953 for (const ImWchar* p = text_selected_begin; p < text_selected_end; )
4954 {
4955 if (rect_pos.y > clip_rect.w + g.FontSize)
4956 break;
4957 if (rect_pos.y < clip_rect.y)
4958 {
4959 //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit
4960 //p = p ? p + 1 : text_selected_end;
4961 while (p < text_selected_end)
4962 if (*p++ == '\n')
4963 break;
4964 }
4965 else
4966 {
4967 ImVec2 rect_size = InputTextCalcTextSizeW(ctx: &g, text_begin: p, text_end: text_selected_end, remaining: &p, NULL, stop_on_new_line: true);
4968 if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
4969 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
4970 rect.ClipWith(r: clip_rect);
4971 if (rect.Overlaps(r: clip_rect))
4972 draw_window->DrawList->AddRectFilled(p_min: rect.Min, p_max: rect.Max, col: bg_color);
4973 }
4974 rect_pos.x = draw_pos.x - draw_scroll.x;
4975 rect_pos.y += g.FontSize;
4976 }
4977 }
4978
4979 // 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.
4980 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
4981 {
4982 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
4983 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);
4984 }
4985
4986 // Draw blinking cursor
4987 if (render_cursor)
4988 {
4989 state->CursorAnim += io.DeltaTime;
4990 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
4991 ImVec2 cursor_screen_pos = ImTrunc(v: draw_pos + cursor_offset - draw_scroll);
4992 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);
4993 if (cursor_is_visible && cursor_screen_rect.Overlaps(r: clip_rect))
4994 draw_window->DrawList->AddLine(p1: cursor_screen_rect.Min, p2: cursor_screen_rect.GetBL(), col: GetColorU32(idx: ImGuiCol_Text));
4995
4996 // 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.)
4997 if (!is_readonly)
4998 {
4999 g.PlatformImeData.WantVisible = true;
5000 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5001 g.PlatformImeData.InputLineHeight = g.FontSize;
5002 g.PlatformImeViewport = window->Viewport->ID;
5003 }
5004 }
5005 }
5006 else
5007 {
5008 // Render text only (no selection, no cursor)
5009 if (is_multiline)
5010 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(text_begin: buf_display, out_text_end: &buf_display_end) * g.FontSize); // We don't need width
5011 else if (!is_displaying_hint && g.ActiveId == id)
5012 buf_display_end = buf_display + state->CurLenA;
5013 else if (!is_displaying_hint)
5014 buf_display_end = buf_display + strlen(s: buf_display);
5015
5016 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5017 {
5018 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5019 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);
5020 }
5021 }
5022
5023 if (is_password && !is_displaying_hint)
5024 PopFont();
5025
5026 if (is_multiline)
5027 {
5028 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)...
5029 Dummy(size: ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5030 g.NextItemData.ItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5031 EndChild();
5032 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5033
5034 // ...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...
5035 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5036 EndGroup();
5037 if (g.LastItemData.ID == 0)
5038 {
5039 g.LastItemData.ID = id;
5040 g.LastItemData.InFlags = item_data_backup.InFlags;
5041 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5042 }
5043 }
5044
5045 // Log as text
5046 if (g.LogEnabled && (!is_password || is_displaying_hint))
5047 {
5048 LogSetNextTextDecoration(prefix: "{", suffix: "}");
5049 LogRenderedText(ref_pos: &draw_pos, text: buf_display, text_end: buf_display_end);
5050 }
5051
5052 if (label_size.x > 0)
5053 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
5054
5055 if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
5056 MarkItemEdited(id);
5057
5058 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5059 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5060 return validated;
5061 else
5062 return value_changed;
5063}
5064
5065void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5066{
5067#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5068 ImGuiContext& g = *GImGui;
5069 ImStb::STB_TexteditState* stb_state = &state->Stb;
5070 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5071 Text(fmt: "ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5072 DebugLocateItemOnHover(target_id: state->ID);
5073 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);
5074 Text(fmt: "has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5075 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);
5076 if (BeginChild(str_id: "undopoints", size: ImVec2(0.0f, GetTextLineHeight() * 10), child_flags: ImGuiChildFlags_Border | ImGuiChildFlags_ResizeY)) // Visualize undo state
5077 {
5078 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(0, 0));
5079 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5080 {
5081 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5082 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5083 if (undo_rec_type == ' ')
5084 BeginDisabled();
5085 char buf[64] = "";
5086 if (undo_rec_type != ' ' && undo_rec->char_storage != -1)
5087 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);
5088 Text(fmt: "%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"",
5089 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf);
5090 if (undo_rec_type == ' ')
5091 EndDisabled();
5092 }
5093 PopStyleVar();
5094 }
5095 EndChild();
5096#else
5097 IM_UNUSED(state);
5098#endif
5099}
5100
5101//-------------------------------------------------------------------------
5102// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5103//-------------------------------------------------------------------------
5104// - ColorEdit3()
5105// - ColorEdit4()
5106// - ColorPicker3()
5107// - RenderColorRectWithAlphaCheckerboard() [Internal]
5108// - ColorPicker4()
5109// - ColorButton()
5110// - SetColorEditOptions()
5111// - ColorTooltip() [Internal]
5112// - ColorEditOptionsPopup() [Internal]
5113// - ColorPickerOptionsPopup() [Internal]
5114//-------------------------------------------------------------------------
5115
5116bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5117{
5118 return ColorEdit4(label, col, flags: flags | ImGuiColorEditFlags_NoAlpha);
5119}
5120
5121static void ColorEditRestoreH(const float* col, float* H)
5122{
5123 ImGuiContext& g = *GImGui;
5124 IM_ASSERT(g.ColorEditCurrentID != 0);
5125 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5126 return;
5127 *H = g.ColorEditSavedHue;
5128}
5129
5130// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5131// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5132static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5133{
5134 ImGuiContext& g = *GImGui;
5135 IM_ASSERT(g.ColorEditCurrentID != 0);
5136 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5137 return;
5138
5139 // When S == 0, H is undefined.
5140 // When H == 1 it wraps around to 0.
5141 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5142 *H = g.ColorEditSavedHue;
5143
5144 // When V == 0, S is undefined.
5145 if (*V == 0.0f)
5146 *S = g.ColorEditSavedSat;
5147}
5148
5149// Edit colors components (each component in 0.0f..1.0f range).
5150// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5151// 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.
5152bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5153{
5154 ImGuiWindow* window = GetCurrentWindow();
5155 if (window->SkipItems)
5156 return false;
5157
5158 ImGuiContext& g = *GImGui;
5159 const ImGuiStyle& style = g.Style;
5160 const float square_sz = GetFrameHeight();
5161 const char* label_display_end = FindRenderedTextEnd(text: label);
5162 float w_full = CalcItemWidth();
5163 g.NextItemData.ClearFlags();
5164
5165 BeginGroup();
5166 PushID(str_id: label);
5167 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5168 if (set_current_color_edit_id)
5169 g.ColorEditCurrentID = window->IDStack.back();
5170
5171 // If we're not showing any slider there's no point in doing any HSV conversions
5172 const ImGuiColorEditFlags flags_untouched = flags;
5173 if (flags & ImGuiColorEditFlags_NoInputs)
5174 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5175
5176 // Context menu: display and modify options (before defaults are applied)
5177 if (!(flags & ImGuiColorEditFlags_NoOptions))
5178 ColorEditOptionsPopup(col, flags);
5179
5180 // Read stored options
5181 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5182 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5183 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5184 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5185 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5186 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5187 if (!(flags & ImGuiColorEditFlags_InputMask_))
5188 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5189 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5190 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5191 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5192
5193 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5194 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5195 const int components = alpha ? 4 : 3;
5196 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5197 const float w_inputs = ImMax(lhs: w_full - w_button, rhs: 1.0f);
5198 w_full = w_inputs + w_button;
5199
5200 // Convert to the formats we need
5201 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5202 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5203 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5204 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5205 {
5206 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5207 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5208 ColorEditRestoreHS(col, H: &f[0], S: &f[1], V: &f[2]);
5209 }
5210 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]) };
5211
5212 bool value_changed = false;
5213 bool value_changed_as_float = false;
5214
5215 const ImVec2 pos = window->DC.CursorPos;
5216 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5217 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5218
5219 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5220 {
5221 // RGB/HSV 0..255 Sliders
5222 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5223
5224 const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize(text: (flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5225 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5226 static const char* fmt_table_int[3][4] =
5227 {
5228 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5229 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5230 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5231 };
5232 static const char* fmt_table_float[3][4] =
5233 {
5234 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5235 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5236 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5237 };
5238 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5239
5240 float prev_split = 0.0f;
5241 for (int n = 0; n < components; n++)
5242 {
5243 if (n > 0)
5244 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5245 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5246 SetNextItemWidth(ImMax(lhs: next_split - prev_split, rhs: 1.0f));
5247 prev_split = next_split;
5248
5249 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5250 if (flags & ImGuiColorEditFlags_Float)
5251 {
5252 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]);
5253 value_changed_as_float |= value_changed;
5254 }
5255 else
5256 {
5257 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]);
5258 }
5259 if (!(flags & ImGuiColorEditFlags_NoOptions))
5260 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5261 }
5262 }
5263 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5264 {
5265 // RGB Hexadecimal Input
5266 char buf[64];
5267 if (alpha)
5268 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));
5269 else
5270 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));
5271 SetNextItemWidth(w_inputs);
5272 if (InputText(label: "##Text", buf, IM_ARRAYSIZE(buf), flags: ImGuiInputTextFlags_CharsUppercase))
5273 {
5274 value_changed = true;
5275 char* p = buf;
5276 while (*p == '#' || ImCharIsBlankA(c: *p))
5277 p++;
5278 i[0] = i[1] = i[2] = 0;
5279 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5280 int r;
5281 if (alpha)
5282 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)
5283 else
5284 r = sscanf(s: p, format: "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5285 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5286 }
5287 if (!(flags & ImGuiColorEditFlags_NoOptions))
5288 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5289 }
5290
5291 ImGuiWindow* picker_active_window = NULL;
5292 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5293 {
5294 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5295 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5296
5297 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5298 if (ColorButton(desc_id: "##ColorButton", col: col_v4, flags))
5299 {
5300 if (!(flags & ImGuiColorEditFlags_NoPicker))
5301 {
5302 // Store current color and open a picker
5303 g.ColorPickerRef = col_v4;
5304 OpenPopup(str_id: "picker");
5305 SetNextWindowPos(pos: g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5306 }
5307 }
5308 if (!(flags & ImGuiColorEditFlags_NoOptions))
5309 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5310
5311 if (BeginPopup(str_id: "picker"))
5312 {
5313 if (g.CurrentWindow->BeginCount == 1)
5314 {
5315 picker_active_window = g.CurrentWindow;
5316 if (label != label_display_end)
5317 {
5318 TextEx(text: label, text_end: label_display_end);
5319 Spacing();
5320 }
5321 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5322 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5323 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5324 value_changed |= ColorPicker4(label: "##picker", col, flags: picker_flags, ref_col: &g.ColorPickerRef.x);
5325 }
5326 EndPopup();
5327 }
5328 }
5329
5330 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5331 {
5332 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5333 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5334 SameLine(offset_from_start_x: 0.0f, spacing: style.ItemInnerSpacing.x);
5335 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5336 TextEx(text: label, text_end: label_display_end);
5337 }
5338
5339 // Convert back
5340 if (value_changed && picker_active_window == NULL)
5341 {
5342 if (!value_changed_as_float)
5343 for (int n = 0; n < 4; n++)
5344 f[n] = i[n] / 255.0f;
5345 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5346 {
5347 g.ColorEditSavedHue = f[0];
5348 g.ColorEditSavedSat = f[1];
5349 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5350 g.ColorEditSavedID = g.ColorEditCurrentID;
5351 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(f[0], f[1], f[2], 0));
5352 }
5353 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5354 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5355
5356 col[0] = f[0];
5357 col[1] = f[1];
5358 col[2] = f[2];
5359 if (alpha)
5360 col[3] = f[3];
5361 }
5362
5363 if (set_current_color_edit_id)
5364 g.ColorEditCurrentID = 0;
5365 PopID();
5366 EndGroup();
5367
5368 // Drag and Drop Target
5369 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5370 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5371 {
5372 bool accepted_drag_drop = false;
5373 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5374 {
5375 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5376 value_changed = accepted_drag_drop = true;
5377 }
5378 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5379 {
5380 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * components);
5381 value_changed = accepted_drag_drop = true;
5382 }
5383
5384 // Drag-drop payloads are always RGB
5385 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5386 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: col[0], out_s&: col[1], out_v&: col[2]);
5387 EndDragDropTarget();
5388 }
5389
5390 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5391 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5392 g.LastItemData.ID = g.ActiveId;
5393
5394 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5395 MarkItemEdited(id: g.LastItemData.ID);
5396
5397 return value_changed;
5398}
5399
5400bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5401{
5402 float col4[4] = { col[0], col[1], col[2], 1.0f };
5403 if (!ColorPicker4(label, col: col4, flags: flags | ImGuiColorEditFlags_NoAlpha))
5404 return false;
5405 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5406 return true;
5407}
5408
5409// Helper for ColorPicker4()
5410static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5411{
5412 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5413 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));
5414 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x, pos.y), half_sz, direction: ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5415 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));
5416 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));
5417}
5418
5419// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5420// (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.)
5421// 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..)
5422// 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)
5423bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5424{
5425 ImGuiContext& g = *GImGui;
5426 ImGuiWindow* window = GetCurrentWindow();
5427 if (window->SkipItems)
5428 return false;
5429
5430 ImDrawList* draw_list = window->DrawList;
5431 ImGuiStyle& style = g.Style;
5432 ImGuiIO& io = g.IO;
5433
5434 const float width = CalcItemWidth();
5435 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5436 g.NextItemData.ClearFlags();
5437
5438 PushID(str_id: label);
5439 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5440 if (set_current_color_edit_id)
5441 g.ColorEditCurrentID = window->IDStack.back();
5442 BeginGroup();
5443
5444 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5445 flags |= ImGuiColorEditFlags_NoSmallPreview;
5446
5447 // Context menu: display and store options.
5448 if (!(flags & ImGuiColorEditFlags_NoOptions))
5449 ColorPickerOptionsPopup(ref_col: col, flags);
5450
5451 // Read stored options
5452 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5453 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5454 if (!(flags & ImGuiColorEditFlags_InputMask_))
5455 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5456 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5457 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5458 if (!(flags & ImGuiColorEditFlags_NoOptions))
5459 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5460
5461 // Setup
5462 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5463 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5464 ImVec2 picker_pos = window->DC.CursorPos;
5465 float square_sz = GetFrameHeight();
5466 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5467 float sv_picker_size = ImMax(lhs: bars_width * 1, rhs: width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5468 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5469 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5470 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5471
5472 float backup_initial_col[4];
5473 memcpy(dest: backup_initial_col, src: col, n: components * sizeof(float));
5474
5475 float wheel_thickness = sv_picker_size * 0.08f;
5476 float wheel_r_outer = sv_picker_size * 0.50f;
5477 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5478 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5479
5480 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5481 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5482 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5483 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5484 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5485
5486 float H = col[0], S = col[1], V = col[2];
5487 float R = col[0], G = col[1], B = col[2];
5488 if (flags & ImGuiColorEditFlags_InputRGB)
5489 {
5490 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5491 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5492 ColorEditRestoreHS(col, H: &H, S: &S, V: &V);
5493 }
5494 else if (flags & ImGuiColorEditFlags_InputHSV)
5495 {
5496 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5497 }
5498
5499 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5500
5501 PushItemFlag(option: ImGuiItemFlags_NoNav, enabled: true);
5502 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5503 {
5504 // Hue wheel + SV triangle logic
5505 InvisibleButton(str_id: "hsv", size_arg: ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5506 if (IsItemActive() && !is_readonly)
5507 {
5508 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5509 ImVec2 current_off = g.IO.MousePos - wheel_center;
5510 float initial_dist2 = ImLengthSqr(lhs: initial_off);
5511 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5512 {
5513 // Interactive with Hue wheel
5514 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5515 if (H < 0.0f)
5516 H += 1.0f;
5517 value_changed = value_changed_h = true;
5518 }
5519 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5520 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5521 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)))
5522 {
5523 // Interacting with SV triangle
5524 ImVec2 current_off_unrotated = ImRotate(v: current_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5525 if (!ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated))
5526 current_off_unrotated = ImTriangleClosestPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated);
5527 float uu, vv, ww;
5528 ImTriangleBarycentricCoords(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated, out_u&: uu, out_v&: vv, out_w&: ww);
5529 V = ImClamp(v: 1.0f - vv, mn: 0.0001f, mx: 1.0f);
5530 S = ImClamp(v: uu / V, mn: 0.0001f, mx: 1.0f);
5531 value_changed = value_changed_sv = true;
5532 }
5533 }
5534 if (!(flags & ImGuiColorEditFlags_NoOptions))
5535 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5536 }
5537 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5538 {
5539 // SV rectangle logic
5540 InvisibleButton(str_id: "sv", size_arg: ImVec2(sv_picker_size, sv_picker_size));
5541 if (IsItemActive() && !is_readonly)
5542 {
5543 S = ImSaturate(f: (io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5544 V = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5545 ColorEditRestoreH(col, H: &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5546 value_changed = value_changed_sv = true;
5547 }
5548 if (!(flags & ImGuiColorEditFlags_NoOptions))
5549 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5550
5551 // Hue bar logic
5552 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5553 InvisibleButton(str_id: "hue", size_arg: ImVec2(bars_width, sv_picker_size));
5554 if (IsItemActive() && !is_readonly)
5555 {
5556 H = ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5557 value_changed = value_changed_h = true;
5558 }
5559 }
5560
5561 // Alpha bar logic
5562 if (alpha_bar)
5563 {
5564 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5565 InvisibleButton(str_id: "alpha", size_arg: ImVec2(bars_width, sv_picker_size));
5566 if (IsItemActive())
5567 {
5568 col[3] = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5569 value_changed = true;
5570 }
5571 }
5572 PopItemFlag(); // ImGuiItemFlags_NoNav
5573
5574 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5575 {
5576 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5577 BeginGroup();
5578 }
5579
5580 if (!(flags & ImGuiColorEditFlags_NoLabel))
5581 {
5582 const char* label_display_end = FindRenderedTextEnd(text: label);
5583 if (label != label_display_end)
5584 {
5585 if ((flags & ImGuiColorEditFlags_NoSidePreview))
5586 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5587 TextEx(text: label, text_end: label_display_end);
5588 }
5589 }
5590
5591 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5592 {
5593 PushItemFlag(option: ImGuiItemFlags_NoNavDefaultFocus, enabled: true);
5594 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5595 if ((flags & ImGuiColorEditFlags_NoLabel))
5596 Text(fmt: "Current");
5597
5598 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
5599 ColorButton(desc_id: "##current", col: col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2));
5600 if (ref_col != NULL)
5601 {
5602 Text(fmt: "Original");
5603 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5604 if (ColorButton(desc_id: "##original", col: ref_col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2)))
5605 {
5606 memcpy(dest: col, src: ref_col, n: components * sizeof(float));
5607 value_changed = true;
5608 }
5609 }
5610 PopItemFlag();
5611 EndGroup();
5612 }
5613
5614 // Convert back color to RGB
5615 if (value_changed_h || value_changed_sv)
5616 {
5617 if (flags & ImGuiColorEditFlags_InputRGB)
5618 {
5619 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5620 g.ColorEditSavedHue = H;
5621 g.ColorEditSavedSat = S;
5622 g.ColorEditSavedID = g.ColorEditCurrentID;
5623 g.ColorEditSavedColor = ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0));
5624 }
5625 else if (flags & ImGuiColorEditFlags_InputHSV)
5626 {
5627 col[0] = H;
5628 col[1] = S;
5629 col[2] = V;
5630 }
5631 }
5632
5633 // R,G,B and H,S,V slider color editor
5634 bool value_changed_fix_hue_wrap = false;
5635 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5636 {
5637 PushItemWidth(item_width: (alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
5638 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
5639 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
5640 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5641 if (ColorEdit4(label: "##rgb", col, flags: sub_flags | ImGuiColorEditFlags_DisplayRGB))
5642 {
5643 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
5644 // 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)
5645 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
5646 value_changed = true;
5647 }
5648 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5649 value_changed |= ColorEdit4(label: "##hsv", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHSV);
5650 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5651 value_changed |= ColorEdit4(label: "##hex", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHex);
5652 PopItemWidth();
5653 }
5654
5655 // Try to cancel hue wrap (after ColorEdit4 call), if any
5656 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
5657 {
5658 float new_H, new_S, new_V;
5659 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: new_H, out_s&: new_S, out_v&: new_V);
5660 if (new_H <= 0 && H > 0)
5661 {
5662 if (new_V <= 0 && V != new_V)
5663 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]);
5664 else if (new_S <= 0)
5665 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]);
5666 }
5667 }
5668
5669 if (value_changed)
5670 {
5671 if (flags & ImGuiColorEditFlags_InputRGB)
5672 {
5673 R = col[0];
5674 G = col[1];
5675 B = col[2];
5676 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5677 ColorEditRestoreHS(col, H: &H, S: &S, V: &V); // Fix local Hue as display below will use it immediately.
5678 }
5679 else if (flags & ImGuiColorEditFlags_InputHSV)
5680 {
5681 H = col[0];
5682 S = col[1];
5683 V = col[2];
5684 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5685 }
5686 }
5687
5688 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
5689 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
5690 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
5691 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
5692 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) };
5693
5694 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);
5695 ImU32 hue_color32 = ColorConvertFloat4ToU32(in: hue_color_f);
5696 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(in: ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
5697
5698 ImVec2 sv_cursor_pos;
5699
5700 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5701 {
5702 // Render Hue Wheel
5703 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
5704 const int segment_per_arc = ImMax(lhs: 4, rhs: (int)wheel_r_outer / 12);
5705 for (int n = 0; n < 6; n++)
5706 {
5707 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
5708 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
5709 const int vert_start_idx = draw_list->VtxBuffer.Size;
5710 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);
5711 draw_list->PathStroke(col: col_white, flags: 0, thickness: wheel_thickness);
5712 const int vert_end_idx = draw_list->VtxBuffer.Size;
5713
5714 // Paint colors over existing vertices
5715 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
5716 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
5717 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0: col_hues[n], col1: col_hues[n + 1]);
5718 }
5719
5720 // Render Cursor + preview on Hue Wheel
5721 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
5722 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
5723 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);
5724 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
5725 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: hue_cursor_rad); // Lock segment count so the +1 one matches others.
5726 draw_list->AddCircleFilled(center: hue_cursor_pos, radius: hue_cursor_rad, col: hue_color32, num_segments: hue_cursor_segments);
5727 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad + 1, col: col_midgrey, num_segments: hue_cursor_segments);
5728 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad, col: col_white, num_segments: hue_cursor_segments);
5729
5730 // Render SV triangle (rotated according to hue)
5731 ImVec2 tra = wheel_center + ImRotate(v: triangle_pa, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5732 ImVec2 trb = wheel_center + ImRotate(v: triangle_pb, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5733 ImVec2 trc = wheel_center + ImRotate(v: triangle_pc, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5734 ImVec2 uv_white = GetFontTexUvWhitePixel();
5735 draw_list->PrimReserve(idx_count: 3, vtx_count: 3);
5736 draw_list->PrimVtx(pos: tra, uv: uv_white, col: hue_color32);
5737 draw_list->PrimVtx(pos: trb, uv: uv_white, col: col_black);
5738 draw_list->PrimVtx(pos: trc, uv: uv_white, col: col_white);
5739 draw_list->AddTriangle(p1: tra, p2: trb, p3: trc, col: col_midgrey, thickness: 1.5f);
5740 sv_cursor_pos = ImLerp(a: ImLerp(a: trc, b: tra, t: ImSaturate(f: S)), b: trb, t: ImSaturate(f: 1 - V));
5741 }
5742 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5743 {
5744 // Render SV Square
5745 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);
5746 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);
5747 RenderFrameBorder(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), rounding: 0.0f);
5748 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
5749 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);
5750
5751 // Render Hue Bar
5752 for (int i = 0; i < 6; ++i)
5753 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]);
5754 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
5755 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);
5756 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);
5757 }
5758
5759 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
5760 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
5761 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(radius: sv_cursor_rad); // Lock segment count so the +1 one matches others.
5762 draw_list->AddCircleFilled(center: sv_cursor_pos, radius: sv_cursor_rad, col: user_col32_striped_of_alpha, num_segments: sv_cursor_segments);
5763 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad + 1, col: col_midgrey, num_segments: sv_cursor_segments);
5764 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad, col: col_white, num_segments: sv_cursor_segments);
5765
5766 // Render alpha bar
5767 if (alpha_bar)
5768 {
5769 float alpha = ImSaturate(f: col[3]);
5770 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
5771 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));
5772 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);
5773 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
5774 RenderFrameBorder(p_min: bar1_bb.Min, p_max: bar1_bb.Max, rounding: 0.0f);
5775 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);
5776 }
5777
5778 EndGroup();
5779
5780 if (value_changed && memcmp(s1: backup_initial_col, s2: col, n: components * sizeof(float)) == 0)
5781 value_changed = false;
5782 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5783 MarkItemEdited(id: g.LastItemData.ID);
5784
5785 if (set_current_color_edit_id)
5786 g.ColorEditCurrentID = 0;
5787 PopID();
5788
5789 return value_changed;
5790}
5791
5792// A little color square. Return true when clicked.
5793// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
5794// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
5795// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
5796bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
5797{
5798 ImGuiWindow* window = GetCurrentWindow();
5799 if (window->SkipItems)
5800 return false;
5801
5802 ImGuiContext& g = *GImGui;
5803 const ImGuiID id = window->GetID(str: desc_id);
5804 const float default_size = GetFrameHeight();
5805 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
5806 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
5807 ItemSize(bb, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
5808 if (!ItemAdd(bb, id))
5809 return false;
5810
5811 bool hovered, held;
5812 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
5813
5814 if (flags & ImGuiColorEditFlags_NoAlpha)
5815 flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
5816
5817 ImVec4 col_rgb = col;
5818 if (flags & ImGuiColorEditFlags_InputHSV)
5819 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);
5820
5821 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
5822 float grid_step = ImMin(lhs: size.x, rhs: size.y) / 2.99f;
5823 float rounding = ImMin(lhs: g.Style.FrameRounding, rhs: grid_step * 0.5f);
5824 ImRect bb_inner = bb;
5825 float off = 0.0f;
5826 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
5827 {
5828 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.
5829 bb_inner.Expand(amount: off);
5830 }
5831 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
5832 {
5833 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
5834 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);
5835 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);
5836 }
5837 else
5838 {
5839 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
5840 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha;
5841 if (col_source.w < 1.0f)
5842 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);
5843 else
5844 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: bb_inner.Max, col: GetColorU32(col: col_source), rounding);
5845 }
5846 RenderNavHighlight(bb, id);
5847 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
5848 {
5849 if (g.Style.FrameBorderSize > 0.0f)
5850 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding);
5851 else
5852 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
5853 }
5854
5855 // Drag and Drop Source
5856 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
5857 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
5858 {
5859 if (flags & ImGuiColorEditFlags_NoAlpha)
5860 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, data: &col_rgb, sz: sizeof(float) * 3, cond: ImGuiCond_Once);
5861 else
5862 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, data: &col_rgb, sz: sizeof(float) * 4, cond: ImGuiCond_Once);
5863 ColorButton(desc_id, col, flags);
5864 SameLine();
5865 TextEx(text: "Color");
5866 EndDragDropSource();
5867 }
5868
5869 // Tooltip
5870 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(flags: ImGuiHoveredFlags_ForTooltip))
5871 ColorTooltip(text: desc_id, col: &col.x, flags: flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
5872
5873 return pressed;
5874}
5875
5876// Initialize/override default color options
5877void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
5878{
5879 ImGuiContext& g = *GImGui;
5880 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5881 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
5882 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
5883 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
5884 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
5885 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
5886 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
5887 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
5888 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
5889 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
5890 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
5891 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
5892 g.ColorEditOptions = flags;
5893}
5894
5895// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5896void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
5897{
5898 ImGuiContext& g = *GImGui;
5899
5900 if (!BeginTooltipEx(tooltip_flags: ImGuiTooltipFlags_OverridePrevious, extra_window_flags: ImGuiWindowFlags_None))
5901 return;
5902 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
5903 if (text_end > text)
5904 {
5905 TextEx(text, text_end);
5906 Separator();
5907 }
5908
5909 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
5910 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5911 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]);
5912 ColorButton(desc_id: "##preview", col: cf, flags: (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, size_arg: sz);
5913 SameLine();
5914 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
5915 {
5916 if (flags & ImGuiColorEditFlags_NoAlpha)
5917 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]);
5918 else
5919 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]);
5920 }
5921 else if (flags & ImGuiColorEditFlags_InputHSV)
5922 {
5923 if (flags & ImGuiColorEditFlags_NoAlpha)
5924 Text(fmt: "H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
5925 else
5926 Text(fmt: "H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
5927 }
5928 EndTooltip();
5929}
5930
5931void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
5932{
5933 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
5934 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
5935 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup(str_id: "context"))
5936 return;
5937 ImGuiContext& g = *GImGui;
5938 g.LockMarkEdited++;
5939 ImGuiColorEditFlags opts = g.ColorEditOptions;
5940 if (allow_opt_inputs)
5941 {
5942 if (RadioButton(label: "RGB", active: (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
5943 if (RadioButton(label: "HSV", active: (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
5944 if (RadioButton(label: "Hex", active: (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
5945 }
5946 if (allow_opt_datatype)
5947 {
5948 if (allow_opt_inputs) Separator();
5949 if (RadioButton(label: "0..255", active: (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
5950 if (RadioButton(label: "0.00..1.00", active: (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
5951 }
5952
5953 if (allow_opt_inputs || allow_opt_datatype)
5954 Separator();
5955 if (Button(label: "Copy as..", size_arg: ImVec2(-1, 0)))
5956 OpenPopup(str_id: "Copy");
5957 if (BeginPopup(str_id: "Copy"))
5958 {
5959 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]);
5960 char buf[64];
5961 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5962 if (Selectable(label: buf))
5963 SetClipboardText(buf);
5964 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%d,%d,%d,%d)", cr, cg, cb, ca);
5965 if (Selectable(label: buf))
5966 SetClipboardText(buf);
5967 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", cr, cg, cb);
5968 if (Selectable(label: buf))
5969 SetClipboardText(buf);
5970 if (!(flags & ImGuiColorEditFlags_NoAlpha))
5971 {
5972 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", cr, cg, cb, ca);
5973 if (Selectable(label: buf))
5974 SetClipboardText(buf);
5975 }
5976 EndPopup();
5977 }
5978
5979 g.ColorEditOptions = opts;
5980 EndPopup();
5981 g.LockMarkEdited--;
5982}
5983
5984void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
5985{
5986 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
5987 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
5988 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup(str_id: "context"))
5989 return;
5990 ImGuiContext& g = *GImGui;
5991 g.LockMarkEdited++;
5992 if (allow_opt_picker)
5993 {
5994 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
5995 PushItemWidth(item_width: picker_size.x);
5996 for (int picker_type = 0; picker_type < 2; picker_type++)
5997 {
5998 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
5999 if (picker_type > 0) Separator();
6000 PushID(int_id: picker_type);
6001 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6002 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6003 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6004 ImVec2 backup_pos = GetCursorScreenPos();
6005 if (Selectable(label: "##selectable", selected: false, flags: 0, size: picker_size)) // By default, Selectable() is closing popup
6006 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6007 SetCursorScreenPos(backup_pos);
6008 ImVec4 previewing_ref_col;
6009 memcpy(dest: &previewing_ref_col, src: ref_col, n: sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6010 ColorPicker4(label: "##previewing_picker", col: &previewing_ref_col.x, flags: picker_flags);
6011 PopID();
6012 }
6013 PopItemWidth();
6014 }
6015 if (allow_opt_alpha_bar)
6016 {
6017 if (allow_opt_picker) Separator();
6018 CheckboxFlags(label: "Alpha Bar", flags: &g.ColorEditOptions, flags_value: ImGuiColorEditFlags_AlphaBar);
6019 }
6020 EndPopup();
6021 g.LockMarkEdited--;
6022}
6023
6024//-------------------------------------------------------------------------
6025// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6026//-------------------------------------------------------------------------
6027// - TreeNode()
6028// - TreeNodeV()
6029// - TreeNodeEx()
6030// - TreeNodeExV()
6031// - TreeNodeBehavior() [Internal]
6032// - TreePush()
6033// - TreePop()
6034// - GetTreeNodeToLabelSpacing()
6035// - SetNextItemOpen()
6036// - CollapsingHeader()
6037//-------------------------------------------------------------------------
6038
6039bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6040{
6041 va_list args;
6042 va_start(args, fmt);
6043 bool is_open = TreeNodeExV(str_id, flags: 0, fmt, args);
6044 va_end(args);
6045 return is_open;
6046}
6047
6048bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6049{
6050 va_list args;
6051 va_start(args, fmt);
6052 bool is_open = TreeNodeExV(ptr_id, flags: 0, fmt, args);
6053 va_end(args);
6054 return is_open;
6055}
6056
6057bool ImGui::TreeNode(const char* label)
6058{
6059 ImGuiWindow* window = GetCurrentWindow();
6060 if (window->SkipItems)
6061 return false;
6062 return TreeNodeBehavior(id: window->GetID(str: label), flags: 0, label, NULL);
6063}
6064
6065bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6066{
6067 return TreeNodeExV(str_id, flags: 0, fmt, args);
6068}
6069
6070bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6071{
6072 return TreeNodeExV(ptr_id, flags: 0, fmt, args);
6073}
6074
6075bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6076{
6077 ImGuiWindow* window = GetCurrentWindow();
6078 if (window->SkipItems)
6079 return false;
6080
6081 return TreeNodeBehavior(id: window->GetID(str: label), flags, label, NULL);
6082}
6083
6084bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6085{
6086 va_list args;
6087 va_start(args, fmt);
6088 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6089 va_end(args);
6090 return is_open;
6091}
6092
6093bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6094{
6095 va_list args;
6096 va_start(args, fmt);
6097 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6098 va_end(args);
6099 return is_open;
6100}
6101
6102bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6103{
6104 ImGuiWindow* window = GetCurrentWindow();
6105 if (window->SkipItems)
6106 return false;
6107
6108 const char* label, *label_end;
6109 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6110 return TreeNodeBehavior(id: window->GetID(str: str_id), flags, label, label_end);
6111}
6112
6113bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6114{
6115 ImGuiWindow* window = GetCurrentWindow();
6116 if (window->SkipItems)
6117 return false;
6118
6119 const char* label, *label_end;
6120 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
6121 return TreeNodeBehavior(id: window->GetID(ptr: ptr_id), flags, label, label_end);
6122}
6123
6124void ImGui::TreeNodeSetOpen(ImGuiID id, bool open)
6125{
6126 ImGuiContext& g = *GImGui;
6127 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6128 storage->SetInt(key: id, val: open ? 1 : 0);
6129}
6130
6131bool ImGui::TreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
6132{
6133 if (flags & ImGuiTreeNodeFlags_Leaf)
6134 return true;
6135
6136 // We only write to the tree storage if the user clicks (or explicitly use the SetNextItemOpen function)
6137 ImGuiContext& g = *GImGui;
6138 ImGuiWindow* window = g.CurrentWindow;
6139 ImGuiStorage* storage = window->DC.StateStorage;
6140
6141 bool is_open;
6142 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen)
6143 {
6144 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6145 {
6146 is_open = g.NextItemData.OpenVal;
6147 TreeNodeSetOpen(id, open: is_open);
6148 }
6149 else
6150 {
6151 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6152 const int stored_value = storage->GetInt(key: id, default_val: -1);
6153 if (stored_value == -1)
6154 {
6155 is_open = g.NextItemData.OpenVal;
6156 TreeNodeSetOpen(id, open: is_open);
6157 }
6158 else
6159 {
6160 is_open = stored_value != 0;
6161 }
6162 }
6163 }
6164 else
6165 {
6166 is_open = storage->GetInt(key: id, default_val: (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6167 }
6168
6169 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6170 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6171 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6172 is_open = true;
6173
6174 return is_open;
6175}
6176
6177bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6178{
6179 ImGuiWindow* window = GetCurrentWindow();
6180 if (window->SkipItems)
6181 return false;
6182
6183 ImGuiContext& g = *GImGui;
6184 const ImGuiStyle& style = g.Style;
6185 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6186 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(lhs: window->DC.CurrLineTextBaseOffset, rhs: style.FramePadding.y));
6187
6188 if (!label_end)
6189 label_end = FindRenderedTextEnd(text: label);
6190 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
6191
6192 // We vertically grow up to current line height up the typical widget height.
6193 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);
6194 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6195 ImRect frame_bb;
6196 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6197 frame_bb.Min.y = window->DC.CursorPos.y;
6198 frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6199 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6200 if (display_frame)
6201 {
6202 // Framed header expand a little outside the default padding, to the edge of InnerClipRect
6203 // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f)
6204 frame_bb.Min.x -= IM_TRUNC(window->WindowPadding.x * 0.5f - 1.0f);
6205 frame_bb.Max.x += IM_TRUNC(window->WindowPadding.x * 0.5f);
6206 }
6207
6208 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6209 const float text_offset_y = ImMax(lhs: padding.y, rhs: window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6210 const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapsing
6211 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6212 ItemSize(size: ImVec2(text_width, frame_height), text_baseline_y: padding.y);
6213
6214 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6215 ImRect interact_bb = frame_bb;
6216 if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6217 interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f;
6218
6219 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6220 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6221 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6222 if (span_all_columns)
6223 {
6224 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6225 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6226 }
6227
6228 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6229 bool is_open = TreeNodeUpdateNextOpen(id, flags);
6230 bool item_add = ItemAdd(bb: interact_bb, id);
6231 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6232 g.LastItemData.DisplayRect = frame_bb;
6233
6234 if (span_all_columns)
6235 {
6236 window->ClipRect.Min.x = backup_clip_rect_min_x;
6237 window->ClipRect.Max.x = backup_clip_rect_max_x;
6238 }
6239
6240 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
6241 // Store data for the current depth to allow returning to this node from any child item.
6242 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6243 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
6244 // Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6245 if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6246 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6247 {
6248 g.NavTreeNodeStack.resize(new_size: g.NavTreeNodeStack.Size + 1);
6249 ImGuiNavTreeNodeData* nav_tree_node_data = &g.NavTreeNodeStack.back();
6250 nav_tree_node_data->ID = id;
6251 nav_tree_node_data->InFlags = g.LastItemData.InFlags;
6252 nav_tree_node_data->NavRect = g.LastItemData.NavRect;
6253 window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth);
6254 }
6255
6256 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6257 if (!item_add)
6258 {
6259 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6260 TreePushOverrideID(id);
6261 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6262 return is_open;
6263 }
6264
6265 if (span_all_columns)
6266 {
6267 TablePushBackgroundChannel();
6268 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6269 g.LastItemData.ClipRect = window->ClipRect;
6270 }
6271
6272 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6273 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap))
6274 button_flags |= ImGuiButtonFlags_AllowOverlap;
6275 if (!is_leaf)
6276 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6277
6278 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6279 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6280 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6281 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6282 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6283 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6284 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6285 button_flags |= ImGuiButtonFlags_NoKeyModifiers;
6286
6287 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6288 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6289 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6290 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6291 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6292 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6293 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6294 // It is rather standard that arrow click react on Down rather than Up.
6295 // 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.
6296 if (is_mouse_x_over_arrow)
6297 button_flags |= ImGuiButtonFlags_PressedOnClick;
6298 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6299 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6300 else
6301 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6302
6303 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6304 const bool was_selected = selected;
6305
6306 bool hovered, held;
6307 bool pressed = ButtonBehavior(bb: interact_bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6308 bool toggled = false;
6309 if (!is_leaf)
6310 {
6311 if (pressed && g.DragDropHoldJustPressedId != id)
6312 {
6313 if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id))
6314 toggled = true;
6315 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6316 toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6317 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6318 toggled = true;
6319 }
6320 else if (pressed && g.DragDropHoldJustPressedId == id)
6321 {
6322 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6323 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6324 toggled = true;
6325 }
6326
6327 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6328 {
6329 toggled = true;
6330 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6331 NavMoveRequestCancel();
6332 }
6333 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?
6334 {
6335 toggled = true;
6336 NavClearPreferredPosForAxis(axis: ImGuiAxis_X);
6337 NavMoveRequestCancel();
6338 }
6339
6340 if (toggled)
6341 {
6342 is_open = !is_open;
6343 window->DC.StateStorage->SetInt(key: id, val: is_open);
6344 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6345 }
6346 }
6347
6348 // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger.
6349 if (selected != was_selected) //-V547
6350 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6351
6352 // Render
6353 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
6354 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_Compact;
6355 if (display_frame)
6356 {
6357 // Framed type
6358 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6359 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: true, rounding: style.FrameRounding);
6360 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6361 if (flags & ImGuiTreeNodeFlags_Bullet)
6362 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);
6363 else if (!is_leaf)
6364 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);
6365 else // Leaf without bullet, left-adjusted text
6366 text_pos.x -= text_offset_x -padding.x;
6367 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6368 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6369
6370 if (g.LogEnabled)
6371 LogSetNextTextDecoration(prefix: "###", suffix: "###");
6372 }
6373 else
6374 {
6375 // Unframed typed for tree nodes
6376 if (hovered || selected)
6377 {
6378 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6379 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: false);
6380 }
6381 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6382 if (flags & ImGuiTreeNodeFlags_Bullet)
6383 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);
6384 else if (!is_leaf)
6385 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);
6386 if (g.LogEnabled)
6387 LogSetNextTextDecoration(prefix: ">", NULL);
6388 }
6389
6390 if (span_all_columns)
6391 TablePopBackgroundChannel();
6392
6393 // Label
6394 if (display_frame)
6395 RenderTextClipped(pos_min: text_pos, pos_max: frame_bb.Max, text: label, text_end: label_end, text_size_if_known: &label_size);
6396 else
6397 RenderText(pos: text_pos, text: label, text_end: label_end, hide_text_after_hash: false);
6398
6399 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6400 TreePushOverrideID(id);
6401 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6402 return is_open;
6403}
6404
6405void ImGui::TreePush(const char* str_id)
6406{
6407 ImGuiWindow* window = GetCurrentWindow();
6408 Indent();
6409 window->DC.TreeDepth++;
6410 PushID(str_id);
6411}
6412
6413void ImGui::TreePush(const void* ptr_id)
6414{
6415 ImGuiWindow* window = GetCurrentWindow();
6416 Indent();
6417 window->DC.TreeDepth++;
6418 PushID(ptr_id);
6419}
6420
6421void ImGui::TreePushOverrideID(ImGuiID id)
6422{
6423 ImGuiContext& g = *GImGui;
6424 ImGuiWindow* window = g.CurrentWindow;
6425 Indent();
6426 window->DC.TreeDepth++;
6427 PushOverrideID(id);
6428}
6429
6430void ImGui::TreePop()
6431{
6432 ImGuiContext& g = *GImGui;
6433 ImGuiWindow* window = g.CurrentWindow;
6434 Unindent();
6435
6436 window->DC.TreeDepth--;
6437 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6438
6439 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6440 if (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask) // Only set during request
6441 {
6442 ImGuiNavTreeNodeData* nav_tree_node_data = &g.NavTreeNodeStack.back();
6443 IM_ASSERT(nav_tree_node_data->ID == window->IDStack.back());
6444 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6445 NavMoveRequestResolveWithPastTreeNode(result: &g.NavMoveResultLocal, tree_node_data: nav_tree_node_data);
6446 g.NavTreeNodeStack.pop_back();
6447 }
6448 window->DC.TreeJumpToParentOnPopMask &= tree_depth_mask - 1;
6449
6450 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.
6451 PopID();
6452}
6453
6454// Horizontal distance preceding label when using TreeNode() or Bullet()
6455float ImGui::GetTreeNodeToLabelSpacing()
6456{
6457 ImGuiContext& g = *GImGui;
6458 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6459}
6460
6461// Set next TreeNode/CollapsingHeader open state.
6462void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6463{
6464 ImGuiContext& g = *GImGui;
6465 if (g.CurrentWindow->SkipItems)
6466 return;
6467 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen;
6468 g.NextItemData.OpenVal = is_open;
6469 g.NextItemData.OpenCond = cond ? cond : ImGuiCond_Always;
6470}
6471
6472// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6473// 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().
6474bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6475{
6476 ImGuiWindow* window = GetCurrentWindow();
6477 if (window->SkipItems)
6478 return false;
6479
6480 return TreeNodeBehavior(id: window->GetID(str: label), flags: flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6481}
6482
6483// p_visible == NULL : regular collapsing header
6484// 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
6485// p_visible != NULL && *p_visible == false : do not show the header at all
6486// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6487bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6488{
6489 ImGuiWindow* window = GetCurrentWindow();
6490 if (window->SkipItems)
6491 return false;
6492
6493 if (p_visible && !*p_visible)
6494 return false;
6495
6496 ImGuiID id = window->GetID(str: label);
6497 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6498 if (p_visible)
6499 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6500 bool is_open = TreeNodeBehavior(id, flags, label);
6501 if (p_visible != NULL)
6502 {
6503 // Create a small overlapping close button
6504 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6505 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6506 ImGuiContext& g = *GImGui;
6507 ImGuiLastItemData last_item_backup = g.LastItemData;
6508 float button_size = g.FontSize;
6509 float button_x = ImMax(lhs: g.LastItemData.Rect.Min.x, rhs: g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
6510 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
6511 ImGuiID close_button_id = GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id);
6512 if (CloseButton(id: close_button_id, pos: ImVec2(button_x, button_y)))
6513 *p_visible = false;
6514 g.LastItemData = last_item_backup;
6515 }
6516
6517 return is_open;
6518}
6519
6520//-------------------------------------------------------------------------
6521// [SECTION] Widgets: Selectable
6522//-------------------------------------------------------------------------
6523// - Selectable()
6524//-------------------------------------------------------------------------
6525
6526// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6527// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6528// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
6529// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6530bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6531{
6532 ImGuiWindow* window = GetCurrentWindow();
6533 if (window->SkipItems)
6534 return false;
6535
6536 ImGuiContext& g = *GImGui;
6537 const ImGuiStyle& style = g.Style;
6538
6539 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6540 ImGuiID id = window->GetID(str: label);
6541 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6542 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6543 ImVec2 pos = window->DC.CursorPos;
6544 pos.y += window->DC.CurrLineTextBaseOffset;
6545 ItemSize(size, text_baseline_y: 0.0f);
6546
6547 // Fill horizontal space
6548 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6549 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6550 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6551 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6552 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6553 size.x = ImMax(lhs: label_size.x, rhs: max_x - min_x);
6554
6555 // Text stays at the submission position, but bounding box may be extended on both sides
6556 const ImVec2 text_min = pos;
6557 const ImVec2 text_max(min_x + size.x, pos.y + size.y);
6558
6559 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
6560 ImRect bb(min_x, pos.y, text_max.x, text_max.y);
6561 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
6562 {
6563 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
6564 const float spacing_y = style.ItemSpacing.y;
6565 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
6566 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
6567 bb.Min.x -= spacing_L;
6568 bb.Min.y -= spacing_U;
6569 bb.Max.x += (spacing_x - spacing_L);
6570 bb.Max.y += (spacing_y - spacing_U);
6571 }
6572 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
6573
6574 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6575 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6576 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6577 if (span_all_columns)
6578 {
6579 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6580 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6581 }
6582
6583 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
6584 const bool item_add = ItemAdd(bb, id, NULL, extra_flags: disabled_item ? ImGuiItemFlags_Disabled : ImGuiItemFlags_None);
6585 if (span_all_columns)
6586 {
6587 window->ClipRect.Min.x = backup_clip_rect_min_x;
6588 window->ClipRect.Max.x = backup_clip_rect_max_x;
6589 }
6590
6591 if (!item_add)
6592 return false;
6593
6594 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
6595 if (disabled_item && !disabled_global) // Only testing this as an optimization
6596 BeginDisabled();
6597
6598 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
6599 // which would be advantageous since most selectable are not selected.
6600 if (span_all_columns)
6601 {
6602 if (g.CurrentTable)
6603 TablePushBackgroundChannel();
6604 else if (window->DC.CurrentColumns)
6605 PushColumnsBackground();
6606 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6607 g.LastItemData.ClipRect = window->ClipRect;
6608 }
6609
6610 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
6611 ImGuiButtonFlags button_flags = 0;
6612 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
6613 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
6614 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
6615 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
6616 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
6617 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.InFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
6618
6619 const bool was_selected = selected;
6620 bool hovered, held;
6621 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6622
6623 // Auto-select when moved into
6624 // - This will be more fully fleshed in the range-select branch
6625 // - This is not exposed as it won't nicely work with some user side handling of shift/control
6626 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
6627 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
6628 // - (2) usage will fail with clipped items
6629 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
6630 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
6631 if (g.NavJustMovedToId == id)
6632 selected = pressed = true;
6633
6634 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard
6635 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
6636 {
6637 if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
6638 {
6639 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: WindowRectAbsToRel(window, r: bb)); // (bb == NavRect)
6640 g.NavDisableHighlight = true;
6641 }
6642 }
6643 if (pressed)
6644 MarkItemEdited(id);
6645
6646 // In this branch, Selectable() cannot toggle the selection so this will never trigger.
6647 if (selected != was_selected) //-V547
6648 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6649
6650 // Render
6651 if (hovered || selected)
6652 {
6653 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6654 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: false, rounding: 0.0f);
6655 }
6656 if (g.NavId == id)
6657 RenderNavHighlight(bb, id, flags: ImGuiNavHighlightFlags_Compact | ImGuiNavHighlightFlags_NoRounding);
6658
6659 if (span_all_columns)
6660 {
6661 if (g.CurrentTable)
6662 TablePopBackgroundChannel();
6663 else if (window->DC.CurrentColumns)
6664 PopColumnsBackground();
6665 }
6666
6667 RenderTextClipped(pos_min: text_min, pos_max: text_max, text: label, NULL, text_size_if_known: &label_size, align: style.SelectableTextAlign, clip_rect: &bb);
6668
6669 // Automatically close popups
6670 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(g.LastItemData.InFlags & ImGuiItemFlags_SelectableDontClosePopup))
6671 CloseCurrentPopup();
6672
6673 if (disabled_item && !disabled_global)
6674 EndDisabled();
6675
6676 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
6677 return pressed; //-V1020
6678}
6679
6680bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6681{
6682 if (Selectable(label, selected: *p_selected, flags, size_arg))
6683 {
6684 *p_selected = !*p_selected;
6685 return true;
6686 }
6687 return false;
6688}
6689
6690
6691//-------------------------------------------------------------------------
6692// [SECTION] Widgets: Typing-Select support
6693//-------------------------------------------------------------------------
6694
6695// [Experimental] Currently not exposed in public API.
6696// Consume character inputs and return search request, if any.
6697// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
6698// if (ImGui::IsWindowFocused(...))
6699// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
6700// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
6701// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
6702ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
6703{
6704 ImGuiContext& g = *GImGui;
6705 ImGuiTypingSelectState* data = &g.TypingSelectState;
6706 ImGuiTypingSelectRequest* out_request = &data->Request;
6707
6708 // Clear buffer
6709 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
6710 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
6711 if (data->SearchBuffer[0] != 0)
6712 {
6713 bool clear_buffer = false;
6714 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
6715 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
6716 clear_buffer |= g.NavAnyRequest;
6717 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
6718 clear_buffer |= IsKeyPressed(key: ImGuiKey_Escape) || IsKeyPressed(key: ImGuiKey_Enter);
6719 clear_buffer |= IsKeyPressed(key: ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
6720 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
6721 if (clear_buffer)
6722 data->Clear();
6723 }
6724
6725 // Append to buffer
6726 const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
6727 int buffer_len = (int)strlen(s: data->SearchBuffer);
6728 bool select_request = false;
6729 for (ImWchar w : g.IO.InputQueueCharacters)
6730 {
6731 const int w_len = ImTextCountUtf8BytesFromStr(in_text: &w, in_text_end: &w + 1);
6732 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(c: w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
6733 continue;
6734 char w_buf[5];
6735 ImTextCharToUtf8(out_buf: w_buf, c: (unsigned int)w);
6736 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(s1: w_buf, s2: data->SearchBuffer, n: w_len) == 0)
6737 {
6738 select_request = true; // Same character: don't need to append to buffer.
6739 continue;
6740 }
6741 if (data->SingleCharModeLock)
6742 {
6743 data->Clear(); // Different character: clear
6744 buffer_len = 0;
6745 }
6746 memcpy(dest: data->SearchBuffer + buffer_len, src: w_buf, n: w_len + 1); // Append
6747 buffer_len += w_len;
6748 select_request = true;
6749 }
6750 g.IO.InputQueueCharacters.resize(new_size: 0);
6751
6752 // Handle backspace
6753 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(key: ImGuiKey_Backspace, owner_id: 0, flags: ImGuiInputFlags_Repeat))
6754 {
6755 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(in_text_start: data->SearchBuffer, in_text_curr: data->SearchBuffer + buffer_len);
6756 *p = 0;
6757 buffer_len = (int)(p - data->SearchBuffer);
6758 }
6759
6760 // Return request if any
6761 if (buffer_len == 0)
6762 return NULL;
6763 if (select_request)
6764 {
6765 data->FocusScope = g.NavFocusScopeId;
6766 data->LastRequestFrame = g.FrameCount;
6767 data->LastRequestTime = (float)g.Time;
6768 }
6769 out_request->Flags = flags;
6770 out_request->SearchBufferLen = buffer_len;
6771 out_request->SearchBuffer = data->SearchBuffer;
6772 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
6773 out_request->SingleCharMode = false;
6774 out_request->SingleCharSize = 0;
6775
6776 // Calculate if buffer contains the same character repeated.
6777 // - This can be used to implement a special search mode on first character.
6778 // - Performed on UTF-8 codepoint for correctness.
6779 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
6780 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
6781 {
6782 const char* buf_begin = out_request->SearchBuffer;
6783 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
6784 const int c0_len = ImTextCountUtf8BytesFromChar(in_text: buf_begin, in_text_end: buf_end);
6785 const char* p = buf_begin + c0_len;
6786 for (; p < buf_end; p += c0_len)
6787 if (memcmp(s1: buf_begin, s2: p, n: (size_t)c0_len) != 0)
6788 break;
6789 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
6790 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
6791 out_request->SingleCharSize = (ImS8)c0_len;
6792 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.
6793 }
6794
6795 return out_request;
6796}
6797
6798static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
6799{
6800 int match_len = 0;
6801 while (s1 < s1_end && ImToUpper(c: *s1++) == ImToUpper(c: *s2++))
6802 match_len++;
6803 return match_len;
6804}
6805
6806// Default handler for finding a result for typing-select. You may implement your own.
6807// You might want to display a tooltip to visualize the current request SearchBuffer
6808// When SingleCharMode is set:
6809// - it is better to NOT display a tooltip of other on-screen display indicator.
6810// - the index of the currently focused item is required.
6811// if your SetNextItemSelectionData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
6812int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
6813{
6814 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
6815 return -1;
6816 int idx = -1;
6817 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
6818 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
6819 else
6820 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
6821 if (idx != -1)
6822 NavRestoreHighlightAfterMove();
6823 return idx;
6824}
6825
6826// Special handling when a single character is repeated: perform search on a single letter and goes to next.
6827int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
6828{
6829 // FIXME: Assume selection user data is index. Would be extremely practical.
6830 //if (nav_item_idx == -1)
6831 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
6832
6833 int first_match_idx = -1;
6834 bool return_next_match = false;
6835 for (int idx = 0; idx < items_count; idx++)
6836 {
6837 const char* item_name = get_item_name_func(user_data, idx);
6838 if (ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SingleCharSize, s2: item_name) < req->SingleCharSize)
6839 continue;
6840 if (return_next_match) // Return next matching item after current item.
6841 return idx;
6842 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
6843 return idx;
6844 if (first_match_idx == -1) // Record first match for wrapping.
6845 first_match_idx = idx;
6846 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
6847 return_next_match = true;
6848 }
6849 return first_match_idx; // First result
6850}
6851
6852int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
6853{
6854 int longest_match_idx = -1;
6855 int longest_match_len = 0;
6856 for (int idx = 0; idx < items_count; idx++)
6857 {
6858 const char* item_name = get_item_name_func(user_data, idx);
6859 const int match_len = ImStrimatchlen(s1: req->SearchBuffer, s1_end: req->SearchBuffer + req->SearchBufferLen, s2: item_name);
6860 if (match_len <= longest_match_len)
6861 continue;
6862 longest_match_idx = idx;
6863 longest_match_len = match_len;
6864 if (match_len == req->SearchBufferLen)
6865 break;
6866 }
6867 return longest_match_idx;
6868}
6869
6870void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
6871{
6872#ifndef IMGUI_DISABLE_DEBUG_TOOLS
6873 Text(fmt: "SearchBuffer = \"%s\"", data->SearchBuffer);
6874 Text(fmt: "SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
6875 Text(fmt: "LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
6876#else
6877 IM_UNUSED(data);
6878#endif
6879}
6880
6881
6882//-------------------------------------------------------------------------
6883// [SECTION] Widgets: Multi-Select support
6884//-------------------------------------------------------------------------
6885
6886void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
6887{
6888 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
6889 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
6890 ImGuiContext& g = *GImGui;
6891 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
6892 g.NextItemData.SelectionUserData = selection_user_data;
6893}
6894
6895
6896//-------------------------------------------------------------------------
6897// [SECTION] Widgets: ListBox
6898//-------------------------------------------------------------------------
6899// - BeginListBox()
6900// - EndListBox()
6901// - ListBox()
6902//-------------------------------------------------------------------------
6903
6904// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
6905// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
6906// 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).
6907bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
6908{
6909 ImGuiContext& g = *GImGui;
6910 ImGuiWindow* window = GetCurrentWindow();
6911 if (window->SkipItems)
6912 return false;
6913
6914 const ImGuiStyle& style = g.Style;
6915 const ImGuiID id = GetID(str_id: label);
6916 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6917
6918 // Size default to hold ~7.25 items.
6919 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
6920 ImVec2 size = ImTrunc(v: CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
6921 ImVec2 frame_size = ImVec2(size.x, ImMax(lhs: size.y, rhs: label_size.y));
6922 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
6923 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
6924 g.NextItemData.ClearFlags();
6925
6926 if (!IsRectVisible(rect_min: bb.Min, rect_max: bb.Max))
6927 {
6928 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
6929 ItemAdd(bb, id: 0, nav_bb: &frame_bb);
6930 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
6931 return false;
6932 }
6933
6934 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
6935 BeginGroup();
6936 if (label_size.x > 0.0f)
6937 {
6938 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
6939 RenderText(pos: label_pos, text: label);
6940 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: label_pos + label_size);
6941 AlignTextToFramePadding();
6942 }
6943
6944 BeginChild(id, size: frame_bb.GetSize(), child_flags: ImGuiChildFlags_FrameStyle);
6945 return true;
6946}
6947
6948void ImGui::EndListBox()
6949{
6950 ImGuiContext& g = *GImGui;
6951 ImGuiWindow* window = g.CurrentWindow;
6952 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
6953 IM_UNUSED(window);
6954
6955 EndChild();
6956 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
6957}
6958
6959bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
6960{
6961 const bool value_changed = ListBox(label, current_item, getter: Items_ArrayGetter, user_data: (void*)items, items_count, height_in_items: height_items);
6962 return value_changed;
6963}
6964
6965// This is merely a helper around BeginListBox(), EndListBox().
6966// Considering using those directly to submit custom data or store selection differently.
6967bool 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)
6968{
6969 ImGuiContext& g = *GImGui;
6970
6971 // Calculate size from "height_in_items"
6972 if (height_in_items < 0)
6973 height_in_items = ImMin(lhs: items_count, rhs: 7);
6974 float height_in_items_f = height_in_items + 0.25f;
6975 ImVec2 size(0.0f, ImTrunc(f: GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
6976
6977 if (!BeginListBox(label, size_arg: size))
6978 return false;
6979
6980 // Assume all items have even height (= 1 line of text). If you need items of different height,
6981 // you can create a custom version of ListBox() in your code without using the clipper.
6982 bool value_changed = false;
6983 ImGuiListClipper clipper;
6984 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.
6985 while (clipper.Step())
6986 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
6987 {
6988 const char* item_text = getter(user_data, i);
6989 if (item_text == NULL)
6990 item_text = "*Unknown item*";
6991
6992 PushID(int_id: i);
6993 const bool item_selected = (i == *current_item);
6994 if (Selectable(label: item_text, selected: item_selected))
6995 {
6996 *current_item = i;
6997 value_changed = true;
6998 }
6999 if (item_selected)
7000 SetItemDefaultFocus();
7001 PopID();
7002 }
7003 EndListBox();
7004
7005 if (value_changed)
7006 MarkItemEdited(id: g.LastItemData.ID);
7007
7008 return value_changed;
7009}
7010
7011//-------------------------------------------------------------------------
7012// [SECTION] Widgets: PlotLines, PlotHistogram
7013//-------------------------------------------------------------------------
7014// - PlotEx() [Internal]
7015// - PlotLines()
7016// - PlotHistogram()
7017//-------------------------------------------------------------------------
7018// Plot/Graph widgets are not very good.
7019// Consider writing your own, or using a third-party one, see:
7020// - ImPlot https://github.com/epezent/implot
7021// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
7022//-------------------------------------------------------------------------
7023
7024int 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)
7025{
7026 ImGuiContext& g = *GImGui;
7027 ImGuiWindow* window = GetCurrentWindow();
7028 if (window->SkipItems)
7029 return -1;
7030
7031 const ImGuiStyle& style = g.Style;
7032 const ImGuiID id = window->GetID(str: label);
7033
7034 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7035 const ImVec2 frame_size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: label_size.y + style.FramePadding.y * 2.0f);
7036
7037 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
7038 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
7039 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));
7040 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
7041 if (!ItemAdd(bb: total_bb, id: 0, nav_bb: &frame_bb))
7042 return -1;
7043 const bool hovered = ItemHoverable(bb: frame_bb, id, item_flags: g.LastItemData.InFlags);
7044
7045 // Determine scale from values if not specified
7046 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
7047 {
7048 float v_min = FLT_MAX;
7049 float v_max = -FLT_MAX;
7050 for (int i = 0; i < values_count; i++)
7051 {
7052 const float v = values_getter(data, i);
7053 if (v != v) // Ignore NaN values
7054 continue;
7055 v_min = ImMin(lhs: v_min, rhs: v);
7056 v_max = ImMax(lhs: v_max, rhs: v);
7057 }
7058 if (scale_min == FLT_MAX)
7059 scale_min = v_min;
7060 if (scale_max == FLT_MAX)
7061 scale_max = v_max;
7062 }
7063
7064 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
7065
7066 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
7067 int idx_hovered = -1;
7068 if (values_count >= values_count_min)
7069 {
7070 int res_w = ImMin(lhs: (int)frame_size.x, rhs: values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
7071 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
7072
7073 // Tooltip on hover
7074 if (hovered && inner_bb.Contains(p: g.IO.MousePos))
7075 {
7076 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);
7077 const int v_idx = (int)(t * item_count);
7078 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
7079
7080 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
7081 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
7082 if (plot_type == ImGuiPlotType_Lines)
7083 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
7084 else if (plot_type == ImGuiPlotType_Histogram)
7085 SetTooltip("%d: %8.4g", v_idx, v0);
7086 idx_hovered = v_idx;
7087 }
7088
7089 const float t_step = 1.0f / (float)res_w;
7090 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
7091
7092 float v0 = values_getter(data, (0 + values_offset) % values_count);
7093 float t0 = 0.0f;
7094 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate(f: (v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
7095 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
7096
7097 const ImU32 col_base = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
7098 const ImU32 col_hovered = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
7099
7100 for (int n = 0; n < res_w; n++)
7101 {
7102 const float t1 = t0 + t_step;
7103 const int v1_idx = (int)(t0 * item_count + 0.5f);
7104 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
7105 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
7106 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate(f: (v1 - scale_min) * inv_scale) );
7107
7108 // 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.
7109 ImVec2 pos0 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: tp0);
7110 ImVec2 pos1 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
7111 if (plot_type == ImGuiPlotType_Lines)
7112 {
7113 window->DrawList->AddLine(p1: pos0, p2: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
7114 }
7115 else if (plot_type == ImGuiPlotType_Histogram)
7116 {
7117 if (pos1.x >= pos0.x + 2.0f)
7118 pos1.x -= 1.0f;
7119 window->DrawList->AddRectFilled(p_min: pos0, p_max: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
7120 }
7121
7122 t0 = t1;
7123 tp0 = tp1;
7124 }
7125 }
7126
7127 // Text overlay
7128 if (overlay_text)
7129 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));
7130
7131 if (label_size.x > 0.0f)
7132 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), text: label);
7133
7134 // Return hovered index or -1 if none are hovered.
7135 // 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().
7136 return idx_hovered;
7137}
7138
7139struct ImGuiPlotArrayGetterData
7140{
7141 const float* Values;
7142 int Stride;
7143
7144 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
7145};
7146
7147static float Plot_ArrayGetter(void* data, int idx)
7148{
7149 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
7150 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
7151 return v;
7152}
7153
7154void 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)
7155{
7156 ImGuiPlotArrayGetterData data(values, stride);
7157 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);
7158}
7159
7160void 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)
7161{
7162 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
7163}
7164
7165void 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)
7166{
7167 ImGuiPlotArrayGetterData data(values, stride);
7168 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);
7169}
7170
7171void 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)
7172{
7173 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, size_arg: graph_size);
7174}
7175
7176//-------------------------------------------------------------------------
7177// [SECTION] Widgets: Value helpers
7178// Those is not very useful, legacy API.
7179//-------------------------------------------------------------------------
7180// - Value()
7181//-------------------------------------------------------------------------
7182
7183void ImGui::Value(const char* prefix, bool b)
7184{
7185 Text(fmt: "%s: %s", prefix, (b ? "true" : "false"));
7186}
7187
7188void ImGui::Value(const char* prefix, int v)
7189{
7190 Text(fmt: "%s: %d", prefix, v);
7191}
7192
7193void ImGui::Value(const char* prefix, unsigned int v)
7194{
7195 Text(fmt: "%s: %d", prefix, v);
7196}
7197
7198void ImGui::Value(const char* prefix, float v, const char* float_format)
7199{
7200 if (float_format)
7201 {
7202 char fmt[64];
7203 ImFormatString(buf: fmt, IM_ARRAYSIZE(fmt), fmt: "%%s: %s", float_format);
7204 Text(fmt, prefix, v);
7205 }
7206 else
7207 {
7208 Text(fmt: "%s: %.3f", prefix, v);
7209 }
7210}
7211
7212//-------------------------------------------------------------------------
7213// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
7214//-------------------------------------------------------------------------
7215// - ImGuiMenuColumns [Internal]
7216// - BeginMenuBar()
7217// - EndMenuBar()
7218// - BeginMainMenuBar()
7219// - EndMainMenuBar()
7220// - BeginMenu()
7221// - EndMenu()
7222// - MenuItemEx() [Internal]
7223// - MenuItem()
7224//-------------------------------------------------------------------------
7225
7226// Helpers for internal use
7227void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
7228{
7229 if (window_reappearing)
7230 memset(s: Widths, c: 0, n: sizeof(Widths));
7231 Spacing = (ImU16)spacing;
7232 CalcNextTotalWidth(update_offsets: true);
7233 memset(s: Widths, c: 0, n: sizeof(Widths));
7234 TotalWidth = NextTotalWidth;
7235 NextTotalWidth = 0;
7236}
7237
7238void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
7239{
7240 ImU16 offset = 0;
7241 bool want_spacing = false;
7242 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
7243 {
7244 ImU16 width = Widths[i];
7245 if (want_spacing && width > 0)
7246 offset += Spacing;
7247 want_spacing |= (width > 0);
7248 if (update_offsets)
7249 {
7250 if (i == 1) { OffsetLabel = offset; }
7251 if (i == 2) { OffsetShortcut = offset; }
7252 if (i == 3) { OffsetMark = offset; }
7253 }
7254 offset += width;
7255 }
7256 NextTotalWidth = offset;
7257}
7258
7259float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
7260{
7261 Widths[0] = ImMax(lhs: Widths[0], rhs: (ImU16)w_icon);
7262 Widths[1] = ImMax(lhs: Widths[1], rhs: (ImU16)w_label);
7263 Widths[2] = ImMax(lhs: Widths[2], rhs: (ImU16)w_shortcut);
7264 Widths[3] = ImMax(lhs: Widths[3], rhs: (ImU16)w_mark);
7265 CalcNextTotalWidth(update_offsets: false);
7266 return (float)ImMax(lhs: TotalWidth, rhs: NextTotalWidth);
7267}
7268
7269// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
7270// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
7271// 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.
7272// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
7273bool ImGui::BeginMenuBar()
7274{
7275 ImGuiWindow* window = GetCurrentWindow();
7276 if (window->SkipItems)
7277 return false;
7278 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
7279 return false;
7280
7281 IM_ASSERT(!window->DC.MenuBarAppending);
7282 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
7283 PushID(str_id: "##menubar");
7284
7285 // 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.
7286 // 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.
7287 ImRect bar_rect = window->MenuBarRect();
7288 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));
7289 clip_rect.ClipWith(r: window->OuterRectClipped);
7290 PushClipRect(clip_rect_min: clip_rect.Min, clip_rect_max: clip_rect.Max, intersect_with_current_clip_rect: false);
7291
7292 // 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).
7293 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
7294 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
7295 window->DC.IsSameLine = false;
7296 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
7297 window->DC.MenuBarAppending = true;
7298 AlignTextToFramePadding();
7299 return true;
7300}
7301
7302void ImGui::EndMenuBar()
7303{
7304 ImGuiWindow* window = GetCurrentWindow();
7305 if (window->SkipItems)
7306 return;
7307 ImGuiContext& g = *GImGui;
7308
7309 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
7310 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
7311 {
7312 // Try to find out if the request is for one of our child menu
7313 ImGuiWindow* nav_earliest_child = g.NavWindow;
7314 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
7315 nav_earliest_child = nav_earliest_child->ParentWindow;
7316 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
7317 {
7318 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
7319 // 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)
7320 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
7321 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
7322 FocusWindow(window);
7323 SetNavID(id: window->NavLastIds[layer], nav_layer: layer, focus_scope_id: 0, rect_rel: window->NavRectRel[layer]);
7324 g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection.
7325 g.NavDisableMouseHover = g.NavMousePosDirty = true;
7326 NavMoveRequestForward(move_dir: g.NavMoveDir, clip_dir: g.NavMoveClipDir, move_flags: g.NavMoveFlags, scroll_flags: g.NavMoveScrollFlags); // Repeat
7327 }
7328 }
7329
7330 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
7331 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
7332 IM_ASSERT(window->DC.MenuBarAppending);
7333 PopClipRect();
7334 PopID();
7335 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.
7336
7337 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
7338 ImGuiGroupData& group_data = g.GroupStack.back();
7339 group_data.EmitItem = false;
7340 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
7341 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.
7342 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
7343 window->DC.LayoutType = ImGuiLayoutType_Vertical;
7344 window->DC.IsSameLine = false;
7345 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
7346 window->DC.MenuBarAppending = false;
7347 window->DC.CursorMaxPos = restore_cursor_max_pos;
7348}
7349
7350// Important: calling order matters!
7351// FIXME: Somehow overlapping with docking tech.
7352// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
7353bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
7354{
7355 IM_ASSERT(dir != ImGuiDir_None);
7356
7357 ImGuiWindow* bar_window = FindWindowByName(name);
7358 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
7359 if (bar_window == NULL || bar_window->BeginCount == 0)
7360 {
7361 // Calculate and set window size/position
7362 ImRect avail_rect = viewport->GetBuildWorkRect();
7363 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
7364 ImVec2 pos = avail_rect.Min;
7365 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
7366 pos[axis] = avail_rect.Max[axis] - axis_size;
7367 ImVec2 size = avail_rect.GetSize();
7368 size[axis] = axis_size;
7369 SetNextWindowPos(pos);
7370 SetNextWindowSize(size);
7371
7372 // Report our size into work area (for next frame) using actual window size
7373 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
7374 viewport->BuildWorkOffsetMin[axis] += axis_size;
7375 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
7376 viewport->BuildWorkOffsetMax[axis] -= axis_size;
7377 }
7378
7379 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
7380 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
7381 PushStyleVar(idx: ImGuiStyleVar_WindowRounding, val: 0.0f);
7382 PushStyleVar(idx: ImGuiStyleVar_WindowMinSize, val: ImVec2(0, 0)); // Lift normal size constraint
7383 bool is_open = Begin(name, NULL, flags: window_flags);
7384 PopStyleVar(count: 2);
7385
7386 return is_open;
7387}
7388
7389bool ImGui::BeginMainMenuBar()
7390{
7391 ImGuiContext& g = *GImGui;
7392 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
7393
7394 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
7395 SetCurrentViewport(NULL, viewport);
7396
7397 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
7398 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
7399 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
7400 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(lhs: g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, rhs: 0.0f));
7401 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
7402 float height = GetFrameHeight();
7403 bool is_open = BeginViewportSideBar(name: "##MainMenuBar", viewport_p: viewport, dir: ImGuiDir_Up, axis_size: height, window_flags);
7404 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
7405
7406 if (is_open)
7407 BeginMenuBar();
7408 else
7409 End();
7410 return is_open;
7411}
7412
7413void ImGui::EndMainMenuBar()
7414{
7415 EndMenuBar();
7416
7417 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
7418 // FIXME: With this strategy we won't be able to restore a NULL focus.
7419 ImGuiContext& g = *GImGui;
7420 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
7421 FocusTopMostWindowUnderOne(under_this_window: g.NavWindow, NULL, NULL, flags: ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
7422
7423 End();
7424}
7425
7426static bool IsRootOfOpenMenuSet()
7427{
7428 ImGuiContext& g = *GImGui;
7429 ImGuiWindow* window = g.CurrentWindow;
7430 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
7431 return false;
7432
7433 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
7434 // (e.g. inside menu bar vs loose menu items) based on parent ID.
7435 // This would however prevent the use of e.g. PushID() user code submitting menus.
7436 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
7437 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
7438 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
7439 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
7440 // 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.
7441 // 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
7442 // 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
7443 // it likely won't be a problem anyone runs into.
7444 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
7445 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
7446 return false;
7447 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(window: upper_popup->Window, potential_parent: window, popup_hierarchy: true, dock_hierarchy: false);
7448}
7449
7450bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
7451{
7452 ImGuiWindow* window = GetCurrentWindow();
7453 if (window->SkipItems)
7454 return false;
7455
7456 ImGuiContext& g = *GImGui;
7457 const ImGuiStyle& style = g.Style;
7458 const ImGuiID id = window->GetID(str: label);
7459 bool menu_is_open = IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None);
7460
7461 // 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)
7462 // 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.
7463 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
7464 if (window->Flags & ImGuiWindowFlags_ChildMenu)
7465 window_flags |= ImGuiWindowFlags_ChildWindow;
7466
7467 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
7468 // 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.
7469 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
7470 if (g.MenusIdSubmittedThisFrame.contains(v: id))
7471 {
7472 if (menu_is_open)
7473 menu_is_open = BeginPopupEx(id, extra_flags: window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
7474 else
7475 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
7476 return menu_is_open;
7477 }
7478
7479 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
7480 g.MenusIdSubmittedThisFrame.push_back(v: id);
7481
7482 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7483
7484 // 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)
7485 // This is only done for items for the menu set and not the full parent window.
7486 const bool menuset_is_open = IsRootOfOpenMenuSet();
7487 if (menuset_is_open)
7488 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
7489
7490 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
7491 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
7492 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
7493 ImVec2 popup_pos, pos = window->DC.CursorPos;
7494 PushID(str_id: label);
7495 if (!enabled)
7496 BeginDisabled();
7497 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
7498 bool pressed;
7499
7500 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
7501 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_DontClosePopups;
7502 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
7503 {
7504 // Menu inside an horizontal menu bar
7505 // Selectable extend their highlight by half ItemSpacing in each direction.
7506 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
7507 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight());
7508 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
7509 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
7510 float w = label_size.x;
7511 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7512 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags, size_arg: ImVec2(w, label_size.y));
7513 RenderText(pos: text_pos, text: label);
7514 PopStyleVar();
7515 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().
7516 }
7517 else
7518 {
7519 // Menu inside a regular/vertical menu
7520 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
7521 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
7522 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
7523 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
7524 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
7525 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
7526 float extra_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
7527 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7528 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
7529 RenderText(pos: text_pos, text: label);
7530 if (icon_w > 0.0f)
7531 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
7532 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);
7533 }
7534 if (!enabled)
7535 EndDisabled();
7536
7537 const bool hovered = (g.HoveredId == id) && enabled && !g.NavDisableMouseHover;
7538 if (menuset_is_open)
7539 PopItemFlag();
7540
7541 bool want_open = false;
7542 bool want_open_nav_init = false;
7543 bool want_close = false;
7544 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
7545 {
7546 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
7547 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
7548 bool moving_toward_child_menu = false;
7549 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
7550 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
7551 if (g.HoveredWindow == window && child_menu_window != NULL)
7552 {
7553 const float ref_unit = g.FontSize; // FIXME-DPI
7554 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
7555 const ImRect next_window_rect = child_menu_window->Rect();
7556 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
7557 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
7558 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
7559 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.
7560 ta.x += child_dir * -0.5f;
7561 tb.x += child_dir * ref_unit;
7562 tc.x += child_dir * ref_unit;
7563 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
7564 tc.y = ta.y + ImMin(lhs: (tc.y + pad_farmost_h) - ta.y, rhs: +ref_unit * 8.0f);
7565 moving_toward_child_menu = ImTriangleContainsPoint(a: ta, b: tb, c: tc, p: g.IO.MousePos);
7566 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
7567 }
7568
7569 // 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)
7570 // 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.
7571 // (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.)
7572 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover && g.ActiveId == 0)
7573 want_close = true;
7574
7575 // Open
7576 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
7577 if (!menu_is_open && pressed) // Click/activate to open
7578 want_open = true;
7579 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
7580 want_open = true;
7581 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
7582 want_open = true;
7583 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
7584 {
7585 want_open = want_open_nav_init = true;
7586 NavMoveRequestCancel();
7587 NavRestoreHighlightAfterMove();
7588 }
7589 }
7590 else
7591 {
7592 // Menu bar
7593 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
7594 {
7595 want_close = true;
7596 want_open = menu_is_open = false;
7597 }
7598 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
7599 {
7600 want_open = true;
7601 }
7602 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
7603 {
7604 want_open = true;
7605 NavMoveRequestCancel();
7606 }
7607 }
7608
7609 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.. }'
7610 want_close = true;
7611 if (want_close && IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None))
7612 ClosePopupToLevel(remaining: g.BeginPopupStack.Size, restore_focus_to_window_under_popup: true);
7613
7614 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
7615 PopID();
7616
7617 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
7618 {
7619 // 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.
7620 OpenPopup(str_id: label);
7621 }
7622 else if (want_open)
7623 {
7624 menu_is_open = true;
7625 OpenPopup(str_id: label, popup_flags: ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
7626 }
7627
7628 if (menu_is_open)
7629 {
7630 ImGuiLastItemData last_item_in_parent = g.LastItemData;
7631 SetNextWindowPos(pos: popup_pos, cond: ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
7632 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
7633 menu_is_open = BeginPopupEx(id, extra_flags: window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
7634 PopStyleVar();
7635 if (menu_is_open)
7636 {
7637 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
7638 // Perform an init request in the case the popup was already open (via a previous mouse hover)
7639 if (want_open && want_open_nav_init && !g.NavInitRequest)
7640 {
7641 FocusWindow(window: g.CurrentWindow, flags: ImGuiFocusRequestFlags_UnlessBelowModal);
7642 NavInitWindow(window: g.CurrentWindow, force_reinit: false);
7643 }
7644
7645 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
7646 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
7647 g.LastItemData = last_item_in_parent;
7648 if (g.HoveredWindow == window)
7649 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
7650 }
7651 }
7652 else
7653 {
7654 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
7655 }
7656
7657 return menu_is_open;
7658}
7659
7660bool ImGui::BeginMenu(const char* label, bool enabled)
7661{
7662 return BeginMenuEx(label, NULL, enabled);
7663}
7664
7665void ImGui::EndMenu()
7666{
7667 // Nav: When a left move request our menu failed, close ourselves.
7668 ImGuiContext& g = *GImGui;
7669 ImGuiWindow* window = g.CurrentWindow;
7670 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
7671 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
7672 if (window->BeginCount == window->BeginCountPreviousFrame)
7673 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
7674 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
7675 {
7676 ClosePopupToLevel(remaining: g.BeginPopupStack.Size - 1, restore_focus_to_window_under_popup: true);
7677 NavMoveRequestCancel();
7678 }
7679
7680 EndPopup();
7681}
7682
7683bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
7684{
7685 ImGuiWindow* window = GetCurrentWindow();
7686 if (window->SkipItems)
7687 return false;
7688
7689 ImGuiContext& g = *GImGui;
7690 ImGuiStyle& style = g.Style;
7691 ImVec2 pos = window->DC.CursorPos;
7692 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7693
7694 // See BeginMenuEx() for comments about this.
7695 const bool menuset_is_open = IsRootOfOpenMenuSet();
7696 if (menuset_is_open)
7697 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
7698
7699 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
7700 // 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.
7701 bool pressed;
7702 PushID(str_id: label);
7703 if (!enabled)
7704 BeginDisabled();
7705
7706 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
7707 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
7708 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
7709 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
7710 {
7711 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
7712 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
7713 float w = label_size.x;
7714 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
7715 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7716 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
7717 pressed = Selectable(label: "", selected, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
7718 PopStyleVar();
7719 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
7720 RenderText(pos: text_pos, text: label);
7721 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().
7722 }
7723 else
7724 {
7725 // Menu item inside a vertical menu
7726 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
7727 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
7728 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
7729 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(text: shortcut, NULL).x : 0.0f;
7730 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
7731 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
7732 float stretch_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
7733 pressed = Selectable(label: "", selected: false, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, label_size.y));
7734 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
7735 {
7736 RenderText(pos: pos + ImVec2(offsets->OffsetLabel, 0.0f), text: label);
7737 if (icon_w > 0.0f)
7738 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
7739 if (shortcut_w > 0.0f)
7740 {
7741 PushStyleColor(idx: ImGuiCol_Text, col: style.Colors[ImGuiCol_TextDisabled]);
7742 RenderText(pos: pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), text: shortcut, NULL, hide_text_after_hash: false);
7743 PopStyleColor();
7744 }
7745 if (selected)
7746 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);
7747 }
7748 }
7749 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
7750 if (!enabled)
7751 EndDisabled();
7752 PopID();
7753 if (menuset_is_open)
7754 PopItemFlag();
7755
7756 return pressed;
7757}
7758
7759bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
7760{
7761 return MenuItemEx(label, NULL, shortcut, selected, enabled);
7762}
7763
7764bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
7765{
7766 if (MenuItemEx(label, NULL, shortcut, selected: p_selected ? *p_selected : false, enabled))
7767 {
7768 if (p_selected)
7769 *p_selected = !*p_selected;
7770 return true;
7771 }
7772 return false;
7773}
7774
7775//-------------------------------------------------------------------------
7776// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
7777//-------------------------------------------------------------------------
7778// - BeginTabBar()
7779// - BeginTabBarEx() [Internal]
7780// - EndTabBar()
7781// - TabBarLayout() [Internal]
7782// - TabBarCalcTabID() [Internal]
7783// - TabBarCalcMaxTabWidth() [Internal]
7784// - TabBarFindTabById() [Internal]
7785// - TabBarFindTabByOrder() [Internal]
7786// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
7787// - TabBarGetCurrentTab() [Internal]
7788// - TabBarGetTabName() [Internal]
7789// - TabBarAddTab() [Internal]
7790// - TabBarRemoveTab() [Internal]
7791// - TabBarCloseTab() [Internal]
7792// - TabBarScrollClamp() [Internal]
7793// - TabBarScrollToTab() [Internal]
7794// - TabBarQueueFocus() [Internal]
7795// - TabBarQueueReorder() [Internal]
7796// - TabBarProcessReorderFromMousePos() [Internal]
7797// - TabBarProcessReorder() [Internal]
7798// - TabBarScrollingButtons() [Internal]
7799// - TabBarTabListPopupButton() [Internal]
7800//-------------------------------------------------------------------------
7801
7802struct ImGuiTabBarSection
7803{
7804 int TabCount; // Number of tabs in this section.
7805 float Width; // Sum of width of tabs in this section (after shrinking down)
7806 float Spacing; // Horizontal spacing at the end of the section.
7807
7808 ImGuiTabBarSection() { memset(s: this, c: 0, n: sizeof(*this)); }
7809};
7810
7811namespace ImGui
7812{
7813 static void TabBarLayout(ImGuiTabBar* tab_bar);
7814 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
7815 static float TabBarCalcMaxTabWidth();
7816 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
7817 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
7818 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
7819 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
7820}
7821
7822ImGuiTabBar::ImGuiTabBar()
7823{
7824 memset(s: this, c: 0, n: sizeof(*this));
7825 CurrFrameVisible = PrevFrameVisible = -1;
7826 LastTabItemIdx = -1;
7827}
7828
7829static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
7830{
7831 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
7832}
7833
7834static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
7835{
7836 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
7837 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
7838 const int a_section = TabItemGetSectionIdx(tab: a);
7839 const int b_section = TabItemGetSectionIdx(tab: b);
7840 if (a_section != b_section)
7841 return a_section - b_section;
7842 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
7843}
7844
7845static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
7846{
7847 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
7848 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
7849 return (int)(a->BeginOrder - b->BeginOrder);
7850}
7851
7852static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
7853{
7854 ImGuiContext& g = *GImGui;
7855 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(n: ref.Index);
7856}
7857
7858static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
7859{
7860 ImGuiContext& g = *GImGui;
7861 if (g.TabBars.Contains(p: tab_bar))
7862 return ImGuiPtrOrIndex(g.TabBars.GetIndex(p: tab_bar));
7863 return ImGuiPtrOrIndex(tab_bar);
7864}
7865
7866bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
7867{
7868 ImGuiContext& g = *GImGui;
7869 ImGuiWindow* window = g.CurrentWindow;
7870 if (window->SkipItems)
7871 return false;
7872
7873 ImGuiID id = window->GetID(str: str_id);
7874 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(key: id);
7875 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);
7876 tab_bar->ID = id;
7877 tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
7878 tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
7879 return BeginTabBarEx(tab_bar, bb: tab_bar_bb, flags: flags | ImGuiTabBarFlags_IsFocused);
7880}
7881
7882bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
7883{
7884 ImGuiContext& g = *GImGui;
7885 ImGuiWindow* window = g.CurrentWindow;
7886 if (window->SkipItems)
7887 return false;
7888
7889 IM_ASSERT(tab_bar->ID != 0);
7890 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
7891 PushOverrideID(id: tab_bar->ID);
7892
7893 // Add to stack
7894 g.CurrentTabBarStack.push_back(v: GetTabBarRefFromTabBar(tab_bar));
7895 g.CurrentTabBar = tab_bar;
7896
7897 // Append with multiple BeginTabBar()/EndTabBar() pairs.
7898 tab_bar->BackupCursorPos = window->DC.CursorPos;
7899 if (tab_bar->CurrFrameVisible == g.FrameCount)
7900 {
7901 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
7902 tab_bar->BeginCount++;
7903 return true;
7904 }
7905
7906 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
7907 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
7908 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
7909 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerByBeginOrder);
7910 tab_bar->TabsAddedNew = false;
7911
7912 // Flags
7913 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
7914 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
7915
7916 tab_bar->Flags = flags;
7917 tab_bar->BarRect = tab_bar_bb;
7918 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
7919 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
7920 tab_bar->CurrFrameVisible = g.FrameCount;
7921 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
7922 tab_bar->CurrTabsContentsHeight = 0.0f;
7923 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
7924 tab_bar->FramePadding = g.Style.FramePadding;
7925 tab_bar->TabsActiveCount = 0;
7926 tab_bar->LastTabItemIdx = -1;
7927 tab_bar->BeginCount = 1;
7928
7929 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
7930 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
7931
7932 // Draw separator
7933 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
7934 const ImU32 col = GetColorU32(idx: (flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive);
7935 if (g.Style.TabBarBorderSize > 0.0f)
7936 {
7937 const float y = tab_bar->BarRect.Max.y;
7938 window->DrawList->AddRectFilled(p_min: ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), p_max: ImVec2(tab_bar->SeparatorMaxX, y), col);
7939 }
7940 return true;
7941}
7942
7943void ImGui::EndTabBar()
7944{
7945 ImGuiContext& g = *GImGui;
7946 ImGuiWindow* window = g.CurrentWindow;
7947 if (window->SkipItems)
7948 return;
7949
7950 ImGuiTabBar* tab_bar = g.CurrentTabBar;
7951 if (tab_bar == NULL)
7952 {
7953 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
7954 return;
7955 }
7956
7957 // Fallback in case no TabItem have been submitted
7958 if (tab_bar->WantLayout)
7959 TabBarLayout(tab_bar);
7960
7961 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
7962 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
7963 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
7964 {
7965 tab_bar->CurrTabsContentsHeight = ImMax(lhs: window->DC.CursorPos.y - tab_bar->BarRect.Max.y, rhs: tab_bar->CurrTabsContentsHeight);
7966 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
7967 }
7968 else
7969 {
7970 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
7971 }
7972 if (tab_bar->BeginCount > 1)
7973 window->DC.CursorPos = tab_bar->BackupCursorPos;
7974
7975 tab_bar->LastTabItemIdx = -1;
7976 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
7977 PopID();
7978
7979 g.CurrentTabBarStack.pop_back();
7980 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(ref: g.CurrentTabBarStack.back());
7981}
7982
7983// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
7984static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
7985{
7986 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
7987}
7988
7989// This is called only once a frame before by the first call to ItemTab()
7990// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
7991static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
7992{
7993 ImGuiContext& g = *GImGui;
7994 tab_bar->WantLayout = false;
7995
7996 // Garbage collect by compacting list
7997 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
7998 int tab_dst_n = 0;
7999 bool need_sort_by_section = false;
8000 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
8001 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
8002 {
8003 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
8004 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
8005 {
8006 // Remove tab
8007 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
8008 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
8009 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
8010 continue;
8011 }
8012 if (tab_dst_n != tab_src_n)
8013 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
8014
8015 tab = &tab_bar->Tabs[tab_dst_n];
8016 tab->IndexDuringLayout = (ImS16)tab_dst_n;
8017
8018 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
8019 int curr_tab_section_n = TabItemGetSectionIdx(tab);
8020 if (tab_dst_n > 0)
8021 {
8022 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
8023 int prev_tab_section_n = TabItemGetSectionIdx(tab: prev_tab);
8024 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
8025 need_sort_by_section = true;
8026 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
8027 need_sort_by_section = true;
8028 }
8029
8030 sections[curr_tab_section_n].TabCount++;
8031 tab_dst_n++;
8032 }
8033 if (tab_bar->Tabs.Size != tab_dst_n)
8034 tab_bar->Tabs.resize(new_size: tab_dst_n);
8035
8036 if (need_sort_by_section)
8037 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerBySection);
8038
8039 // Calculate spacing between sections
8040 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
8041 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
8042
8043 // Setup next selected tab
8044 ImGuiID scroll_to_tab_id = 0;
8045 if (tab_bar->NextSelectedTabId)
8046 {
8047 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
8048 tab_bar->NextSelectedTabId = 0;
8049 scroll_to_tab_id = tab_bar->SelectedTabId;
8050 }
8051
8052 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
8053 if (tab_bar->ReorderRequestTabId != 0)
8054 {
8055 if (TabBarProcessReorder(tab_bar))
8056 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
8057 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
8058 tab_bar->ReorderRequestTabId = 0;
8059 }
8060
8061 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
8062 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
8063 if (tab_list_popup_button)
8064 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
8065 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
8066
8067 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
8068 // (whereas our tabs are stored as: leading, central, trailing)
8069 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
8070 g.ShrinkWidthBuffer.resize(new_size: tab_bar->Tabs.Size);
8071
8072 // Compute ideal tabs widths + store them into shrink buffer
8073 ImGuiTabItem* most_recently_selected_tab = NULL;
8074 int curr_section_n = -1;
8075 bool found_selected_tab_id = false;
8076 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
8077 {
8078 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
8079 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
8080
8081 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
8082 most_recently_selected_tab = tab;
8083 if (tab->ID == tab_bar->SelectedTabId)
8084 found_selected_tab_id = true;
8085 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
8086 scroll_to_tab_id = tab->ID;
8087
8088 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
8089 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
8090 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
8091 const char* tab_name = TabBarGetTabName(tab_bar, tab);
8092 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
8093 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(label: tab_name, has_close_button_or_unsaved_marker).x;
8094
8095 int section_n = TabItemGetSectionIdx(tab);
8096 ImGuiTabBarSection* section = &sections[section_n];
8097 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
8098 curr_section_n = section_n;
8099
8100 // Store data so we can build an array sorted by width if we need to shrink tabs down
8101 IM_MSVC_WARNING_SUPPRESS(6385);
8102 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
8103 shrink_width_item->Index = tab_n;
8104 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
8105 tab->Width = ImMax(lhs: tab->ContentWidth, rhs: 1.0f);
8106 }
8107
8108 // Compute total ideal width (used for e.g. auto-resizing a window)
8109 tab_bar->WidthAllTabsIdeal = 0.0f;
8110 for (int section_n = 0; section_n < 3; section_n++)
8111 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
8112
8113 // Horizontal scrolling buttons
8114 // (note that TabBarScrollButtons() will alter BarRect.Max.x)
8115 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
8116 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
8117 {
8118 scroll_to_tab_id = scroll_and_select_tab->ID;
8119 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
8120 tab_bar->SelectedTabId = scroll_to_tab_id;
8121 }
8122
8123 // Shrink widths if full tabs don't fit in their allocated space
8124 float section_0_w = sections[0].Width + sections[0].Spacing;
8125 float section_1_w = sections[1].Width + sections[1].Spacing;
8126 float section_2_w = sections[2].Width + sections[2].Spacing;
8127 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
8128 float width_excess;
8129 if (central_section_is_visible)
8130 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
8131 else
8132 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
8133
8134 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
8135 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
8136 {
8137 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
8138 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
8139 ShrinkWidths(items: g.ShrinkWidthBuffer.Data + shrink_data_offset, count: shrink_data_count, width_excess);
8140
8141 // Apply shrunk values into tabs and sections
8142 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
8143 {
8144 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
8145 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
8146 if (shrinked_width < 0.0f)
8147 continue;
8148
8149 shrinked_width = ImMax(lhs: 1.0f, rhs: shrinked_width);
8150 int section_n = TabItemGetSectionIdx(tab);
8151 sections[section_n].Width -= (tab->Width - shrinked_width);
8152 tab->Width = shrinked_width;
8153 }
8154 }
8155
8156 // Layout all active tabs
8157 int section_tab_index = 0;
8158 float tab_offset = 0.0f;
8159 tab_bar->WidthAllTabs = 0.0f;
8160 for (int section_n = 0; section_n < 3; section_n++)
8161 {
8162 ImGuiTabBarSection* section = &sections[section_n];
8163 if (section_n == 2)
8164 tab_offset = ImMin(lhs: ImMax(lhs: 0.0f, rhs: tab_bar->BarRect.GetWidth() - section->Width), rhs: tab_offset);
8165
8166 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
8167 {
8168 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
8169 tab->Offset = tab_offset;
8170 tab->NameOffset = -1;
8171 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
8172 }
8173 tab_bar->WidthAllTabs += ImMax(lhs: section->Width + section->Spacing, rhs: 0.0f);
8174 tab_offset += section->Spacing;
8175 section_tab_index += section->TabCount;
8176 }
8177
8178 // Clear name buffers
8179 tab_bar->TabsNames.Buf.resize(new_size: 0);
8180
8181 // If we have lost the selected tab, select the next most recently active one
8182 if (found_selected_tab_id == false)
8183 tab_bar->SelectedTabId = 0;
8184 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
8185 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
8186
8187 // Lock in visible tab
8188 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
8189 tab_bar->VisibleTabWasSubmitted = false;
8190
8191 // CTRL+TAB can override visible tab temporarily
8192 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
8193 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
8194
8195 // Apply request requests
8196 if (scroll_to_tab_id != 0)
8197 TabBarScrollToTab(tab_bar, tab_id: scroll_to_tab_id, sections);
8198 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))
8199 {
8200 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
8201 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
8202 if (TestKeyOwner(key: wheel_key, owner_id: tab_bar->ID) && wheel != 0.0f)
8203 {
8204 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
8205 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
8206 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget - scroll_step);
8207 }
8208 SetKeyOwner(key: wheel_key, owner_id: tab_bar->ID);
8209 }
8210
8211 // Update scrolling
8212 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingAnim);
8213 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget);
8214 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
8215 {
8216 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
8217 // Teleport if we are aiming far off the visible line
8218 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, rhs: 70.0f * g.FontSize);
8219 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
8220 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
8221 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(current: tab_bar->ScrollingAnim, target: tab_bar->ScrollingTarget, speed: g.IO.DeltaTime * tab_bar->ScrollingSpeed);
8222 }
8223 else
8224 {
8225 tab_bar->ScrollingSpeed = 0.0f;
8226 }
8227 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
8228 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
8229
8230 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
8231 ImGuiWindow* window = g.CurrentWindow;
8232 window->DC.CursorPos = tab_bar->BarRect.Min;
8233 ItemSize(size: ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), text_baseline_y: tab_bar->FramePadding.y);
8234 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
8235}
8236
8237// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
8238static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
8239{
8240 if (docked_window != NULL)
8241 {
8242 IM_UNUSED(tab_bar);
8243 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
8244 ImGuiID id = docked_window->TabId;
8245 KeepAliveID(id);
8246 return id;
8247 }
8248 else
8249 {
8250 ImGuiWindow* window = GImGui->CurrentWindow;
8251 return window->GetID(str: label);
8252 }
8253}
8254
8255static float ImGui::TabBarCalcMaxTabWidth()
8256{
8257 ImGuiContext& g = *GImGui;
8258 return g.FontSize * 20.0f;
8259}
8260
8261ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
8262{
8263 if (tab_id != 0)
8264 for (int n = 0; n < tab_bar->Tabs.Size; n++)
8265 if (tab_bar->Tabs[n].ID == tab_id)
8266 return &tab_bar->Tabs[n];
8267 return NULL;
8268}
8269
8270// Order = visible order, not submission order! (which is tab->BeginOrder)
8271ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
8272{
8273 if (order < 0 || order >= tab_bar->Tabs.Size)
8274 return NULL;
8275 return &tab_bar->Tabs[order];
8276}
8277
8278// FIXME: See references to #2304 in TODO.txt
8279ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
8280{
8281 ImGuiTabItem* most_recently_selected_tab = NULL;
8282 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
8283 {
8284 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
8285 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
8286 if (tab->Window && tab->Window->WasActive)
8287 most_recently_selected_tab = tab;
8288 }
8289 return most_recently_selected_tab;
8290}
8291
8292ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
8293{
8294 if (tab_bar->LastTabItemIdx <= 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
8295 return NULL;
8296 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
8297}
8298
8299const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
8300{
8301 if (tab->Window)
8302 return tab->Window->Name;
8303 if (tab->NameOffset == -1)
8304 return "N/A";
8305 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
8306 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
8307}
8308
8309// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
8310// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
8311void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
8312{
8313 ImGuiContext& g = *GImGui;
8314 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
8315 IM_ASSERT(g.CurrentTabBar != tab_bar); // Can't work while the tab bar is active as our tab doesn't have an X offset yet, in theory we could/should test something like (tab_bar->CurrFrameVisible < g.FrameCount) but we'd need to solve why triggers the commented early-out assert in BeginTabBarEx() (probably dock node going from implicit to explicit in same frame)
8316
8317 if (!window->HasCloseButton)
8318 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
8319
8320 ImGuiTabItem new_tab;
8321 new_tab.ID = window->TabId;
8322 new_tab.Flags = tab_flags;
8323 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
8324 if (new_tab.LastFrameVisible == -1)
8325 new_tab.LastFrameVisible = g.FrameCount - 1;
8326 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
8327 tab_bar->Tabs.push_back(v: new_tab);
8328}
8329
8330// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
8331void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
8332{
8333 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
8334 tab_bar->Tabs.erase(it: tab);
8335 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
8336 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
8337 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
8338}
8339
8340// Called on manual closure attempt
8341void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
8342{
8343 if (tab->Flags & ImGuiTabItemFlags_Button)
8344 return; // A button appended with TabItemButton().
8345
8346 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
8347 {
8348 // This will remove a frame of lag for selecting another tab on closure.
8349 // 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
8350 tab->WantClose = true;
8351 if (tab_bar->VisibleTabId == tab->ID)
8352 {
8353 tab->LastFrameVisible = -1;
8354 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
8355 }
8356 }
8357 else
8358 {
8359 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
8360 if (tab_bar->VisibleTabId != tab->ID)
8361 TabBarQueueFocus(tab_bar, tab);
8362 }
8363}
8364
8365static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
8366{
8367 scrolling = ImMin(lhs: scrolling, rhs: tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
8368 return ImMax(lhs: scrolling, rhs: 0.0f);
8369}
8370
8371// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
8372static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
8373{
8374 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
8375 if (tab == NULL)
8376 return;
8377 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
8378 return;
8379
8380 ImGuiContext& g = *GImGui;
8381 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)
8382 int order = TabBarGetTabOrder(tab_bar, tab);
8383
8384 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
8385 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
8386
8387 // We make all tabs positions all relative Sections[0].Width to make code simpler
8388 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
8389 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
8390 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
8391 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
8392 {
8393 // Scroll to the left
8394 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: tab_bar->ScrollingAnim - tab_x2, rhs: 0.0f);
8395 tab_bar->ScrollingTarget = tab_x1;
8396 }
8397 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
8398 {
8399 // Scroll to the right
8400 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: (tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, rhs: 0.0f);
8401 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
8402 }
8403}
8404
8405void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
8406{
8407 tab_bar->NextSelectedTabId = tab->ID;
8408}
8409
8410void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
8411{
8412 IM_ASSERT(offset != 0);
8413 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
8414 tab_bar->ReorderRequestTabId = tab->ID;
8415 tab_bar->ReorderRequestOffset = (ImS16)offset;
8416}
8417
8418void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
8419{
8420 ImGuiContext& g = *GImGui;
8421 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
8422 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
8423 return;
8424
8425 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
8426 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
8427
8428 // Count number of contiguous tabs we are crossing over
8429 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
8430 const int src_idx = tab_bar->Tabs.index_from_ptr(it: src_tab);
8431 int dst_idx = src_idx;
8432 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
8433 {
8434 // Reordered tabs must share the same section
8435 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
8436 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
8437 break;
8438 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
8439 break;
8440 dst_idx = i;
8441
8442 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
8443 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
8444 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
8445 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
8446 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
8447 break;
8448 }
8449
8450 if (dst_idx != src_idx)
8451 TabBarQueueReorder(tab_bar, tab: src_tab, offset: dst_idx - src_idx);
8452}
8453
8454bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
8455{
8456 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_id: tab_bar->ReorderRequestTabId);
8457 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
8458 return false;
8459
8460 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
8461 int tab2_order = TabBarGetTabOrder(tab_bar, tab: tab1) + tab_bar->ReorderRequestOffset;
8462 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
8463 return false;
8464
8465 // Reordered tabs must share the same section
8466 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
8467 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
8468 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
8469 return false;
8470 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
8471 return false;
8472
8473 ImGuiTabItem item_tmp = *tab1;
8474 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
8475 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
8476 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
8477 memmove(dest: dst_tab, src: src_tab, n: move_count * sizeof(ImGuiTabItem));
8478 *tab2 = item_tmp;
8479
8480 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
8481 MarkIniSettingsDirty();
8482 return true;
8483}
8484
8485static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
8486{
8487 ImGuiContext& g = *GImGui;
8488 ImGuiWindow* window = g.CurrentWindow;
8489
8490 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
8491 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
8492
8493 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
8494 //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));
8495
8496 int select_dir = 0;
8497 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
8498 arrow_col.w *= 0.5f;
8499
8500 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
8501 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
8502 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
8503 const float backup_repeat_rate = g.IO.KeyRepeatRate;
8504 g.IO.KeyRepeatDelay = 0.250f;
8505 g.IO.KeyRepeatRate = 0.200f;
8506 float x = ImMax(lhs: tab_bar->BarRect.Min.x, rhs: tab_bar->BarRect.Max.x - scrolling_buttons_width);
8507 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
8508 if (ArrowButtonEx(str_id: "##<", dir: ImGuiDir_Left, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
8509 select_dir = -1;
8510 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
8511 if (ArrowButtonEx(str_id: "##>", dir: ImGuiDir_Right, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
8512 select_dir = +1;
8513 PopStyleColor(count: 2);
8514 g.IO.KeyRepeatRate = backup_repeat_rate;
8515 g.IO.KeyRepeatDelay = backup_repeat_delay;
8516
8517 ImGuiTabItem* tab_to_scroll_to = NULL;
8518 if (select_dir != 0)
8519 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_id: tab_bar->SelectedTabId))
8520 {
8521 int selected_order = TabBarGetTabOrder(tab_bar, tab: tab_item);
8522 int target_order = selected_order + select_dir;
8523
8524 // Skip tab item buttons until another tab item is found or end is reached
8525 while (tab_to_scroll_to == NULL)
8526 {
8527 // If we are at the end of the list, still scroll to make our tab visible
8528 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
8529
8530 // Cross through buttons
8531 // (even if first/last item is a button, return it so we can update the scroll)
8532 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
8533 {
8534 target_order += select_dir;
8535 selected_order += select_dir;
8536 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
8537 }
8538 }
8539 }
8540 window->DC.CursorPos = backup_cursor_pos;
8541 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
8542
8543 return tab_to_scroll_to;
8544}
8545
8546static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
8547{
8548 ImGuiContext& g = *GImGui;
8549 ImGuiWindow* window = g.CurrentWindow;
8550
8551 // We use g.Style.FramePadding.y to match the square ArrowButton size
8552 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
8553 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
8554 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
8555 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
8556
8557 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
8558 arrow_col.w *= 0.5f;
8559 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
8560 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
8561 bool open = BeginCombo(label: "##v", NULL, flags: ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
8562 PopStyleColor(count: 2);
8563
8564 ImGuiTabItem* tab_to_select = NULL;
8565 if (open)
8566 {
8567 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
8568 {
8569 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
8570 if (tab->Flags & ImGuiTabItemFlags_Button)
8571 continue;
8572
8573 const char* tab_name = TabBarGetTabName(tab_bar, tab);
8574 if (Selectable(label: tab_name, selected: tab_bar->SelectedTabId == tab->ID))
8575 tab_to_select = tab;
8576 }
8577 EndCombo();
8578 }
8579
8580 window->DC.CursorPos = backup_cursor_pos;
8581 return tab_to_select;
8582}
8583
8584//-------------------------------------------------------------------------
8585// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
8586//-------------------------------------------------------------------------
8587// - BeginTabItem()
8588// - EndTabItem()
8589// - TabItemButton()
8590// - TabItemEx() [Internal]
8591// - SetTabItemClosed()
8592// - TabItemCalcSize() [Internal]
8593// - TabItemBackground() [Internal]
8594// - TabItemLabelAndCloseButton() [Internal]
8595//-------------------------------------------------------------------------
8596
8597bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
8598{
8599 ImGuiContext& g = *GImGui;
8600 ImGuiWindow* window = g.CurrentWindow;
8601 if (window->SkipItems)
8602 return false;
8603
8604 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8605 if (tab_bar == NULL)
8606 {
8607 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
8608 return false;
8609 }
8610 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
8611
8612 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
8613 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
8614 {
8615 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
8616 PushOverrideID(id: tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
8617 }
8618 return ret;
8619}
8620
8621void ImGui::EndTabItem()
8622{
8623 ImGuiContext& g = *GImGui;
8624 ImGuiWindow* window = g.CurrentWindow;
8625 if (window->SkipItems)
8626 return;
8627
8628 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8629 if (tab_bar == NULL)
8630 {
8631 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
8632 return;
8633 }
8634 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
8635 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
8636 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
8637 PopID();
8638}
8639
8640bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
8641{
8642 ImGuiContext& g = *GImGui;
8643 ImGuiWindow* window = g.CurrentWindow;
8644 if (window->SkipItems)
8645 return false;
8646
8647 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8648 if (tab_bar == NULL)
8649 {
8650 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
8651 return false;
8652 }
8653 return TabItemEx(tab_bar, label, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
8654}
8655
8656bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
8657{
8658 // Layout whole tab bar if not already done
8659 ImGuiContext& g = *GImGui;
8660 if (tab_bar->WantLayout)
8661 {
8662 ImGuiNextItemData backup_next_item_data = g.NextItemData;
8663 TabBarLayout(tab_bar);
8664 g.NextItemData = backup_next_item_data;
8665 }
8666 ImGuiWindow* window = g.CurrentWindow;
8667 if (window->SkipItems)
8668 return false;
8669
8670 const ImGuiStyle& style = g.Style;
8671 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
8672
8673 // If the user called us with *p_open == false, we early out and don't render.
8674 // 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.
8675 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
8676 if (p_open && !*p_open)
8677 {
8678 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
8679 return false;
8680 }
8681
8682 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
8683 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
8684
8685 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
8686 if (flags & ImGuiTabItemFlags_NoCloseButton)
8687 p_open = NULL;
8688 else if (p_open == NULL)
8689 flags |= ImGuiTabItemFlags_NoCloseButton;
8690
8691 // Acquire tab data
8692 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id: id);
8693 bool tab_is_new = false;
8694 if (tab == NULL)
8695 {
8696 tab_bar->Tabs.push_back(v: ImGuiTabItem());
8697 tab = &tab_bar->Tabs.back();
8698 tab->ID = id;
8699 tab_bar->TabsAddedNew = tab_is_new = true;
8700 }
8701 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(it: tab);
8702
8703 // Calculate tab contents size
8704 ImVec2 size = TabItemCalcSize(label, has_close_button_or_unsaved_marker: (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
8705 tab->RequestedWidth = -1.0f;
8706 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth)
8707 size.x = tab->RequestedWidth = g.NextItemData.Width;
8708 if (tab_is_new)
8709 tab->Width = ImMax(lhs: 1.0f, rhs: size.x);
8710 tab->ContentWidth = size.x;
8711 tab->BeginOrder = tab_bar->TabsActiveCount++;
8712
8713 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
8714 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
8715 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
8716 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
8717 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
8718 tab->LastFrameVisible = g.FrameCount;
8719 tab->Flags = flags;
8720 tab->Window = docked_window;
8721
8722 // Append name _WITH_ the zero-terminator
8723 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
8724 if (docked_window != NULL)
8725 {
8726 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
8727 tab->NameOffset = -1;
8728 }
8729 else
8730 {
8731 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
8732 tab_bar->TabsNames.append(str: label, str_end: label + strlen(s: label) + 1);
8733 }
8734
8735 // Update selected tab
8736 if (!is_tab_button)
8737 {
8738 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
8739 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
8740 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
8741 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
8742 TabBarQueueFocus(tab_bar, tab);
8743 }
8744
8745 // Lock visibility
8746 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
8747 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
8748 if (tab_contents_visible)
8749 tab_bar->VisibleTabWasSubmitted = true;
8750
8751 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
8752 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
8753 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
8754 tab_contents_visible = true;
8755
8756 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
8757 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
8758 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
8759 {
8760 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
8761 if (is_tab_button)
8762 return false;
8763 return tab_contents_visible;
8764 }
8765
8766 if (tab_bar->SelectedTabId == id)
8767 tab->LastFrameSelected = g.FrameCount;
8768
8769 // Backup current layout position
8770 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
8771
8772 // Layout
8773 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
8774 size.x = tab->Width;
8775 if (is_central_section)
8776 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
8777 else
8778 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
8779 ImVec2 pos = window->DC.CursorPos;
8780 ImRect bb(pos, pos + size);
8781
8782 // 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)
8783 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
8784 if (want_clip_rect)
8785 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);
8786
8787 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
8788 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
8789 window->DC.CursorMaxPos = backup_cursor_max_pos;
8790
8791 if (!ItemAdd(bb, id))
8792 {
8793 if (want_clip_rect)
8794 PopClipRect();
8795 window->DC.CursorPos = backup_main_cursor_pos;
8796 return tab_contents_visible;
8797 }
8798
8799 // Click to Select a tab
8800 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
8801 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
8802 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
8803 bool hovered, held;
8804 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
8805 if (pressed && !is_tab_button)
8806 TabBarQueueFocus(tab_bar, tab);
8807
8808 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
8809 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
8810 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
8811 g.ActiveIdWindow = docked_window;
8812
8813 // Drag and drop a single floating window node moves it
8814 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
8815 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
8816 if (held && single_floating_window_node && IsMouseDragging(button: 0, lock_threshold: 0.0f))
8817 {
8818 // Move
8819 StartMouseMovingWindow(window: docked_window);
8820 }
8821 else if (held && !tab_appearing && IsMouseDragging(button: 0))
8822 {
8823 // Drag and drop: re-order tabs
8824 int drag_dir = 0;
8825 float drag_distance_from_edge_x = 0.0f;
8826 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
8827 {
8828 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
8829 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
8830 {
8831 drag_dir = -1;
8832 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
8833 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
8834 }
8835 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
8836 {
8837 drag_dir = +1;
8838 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
8839 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
8840 }
8841 }
8842
8843 // Extract a Dockable window out of it's tab bar
8844 const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
8845 if (can_undock)
8846 {
8847 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
8848 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
8849 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
8850 {
8851 float threshold_base = g.FontSize;
8852 float threshold_x = (threshold_base * 2.2f);
8853 float threshold_y = (threshold_base * 1.5f) + ImClamp(v: (ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, mn: 0.0f, mx: threshold_base * 4.0f);
8854 //GetForegroundDrawList()->AddRect(ImVec2(bb.Min.x - threshold_x, bb.Min.y - threshold_y), ImVec2(bb.Max.x + threshold_x, bb.Max.y + threshold_y), IM_COL32_WHITE); // [DEBUG]
8855
8856 float distance_from_edge_y = ImMax(lhs: bb.Min.y - g.IO.MousePos.y, rhs: g.IO.MousePos.y - bb.Max.y);
8857 if (distance_from_edge_y >= threshold_y)
8858 undocking_tab = true;
8859 if (drag_distance_from_edge_x > threshold_x)
8860 if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
8861 undocking_tab = true;
8862 }
8863
8864 if (undocking_tab)
8865 {
8866 // Undock
8867 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
8868 DockContextQueueUndockWindow(ctx: &g, window: docked_window);
8869 g.MovingWindow = docked_window;
8870 SetActiveID(id: g.MovingWindow->MoveId, window: g.MovingWindow);
8871 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
8872 g.ActiveIdNoClearOnFocusLoss = true;
8873 SetActiveIdUsingAllKeyboardKeys();
8874 }
8875 }
8876 }
8877
8878#if 0
8879 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
8880 {
8881 // Enlarge tab display when hovering
8882 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
8883 display_draw_list = GetForegroundDrawList(window);
8884 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
8885 }
8886#endif
8887
8888 // Render tab shape
8889 ImDrawList* display_draw_list = window->DrawList;
8890 const ImU32 tab_col = GetColorU32(idx: (held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabUnfocused));
8891 TabItemBackground(draw_list: display_draw_list, bb, flags, col: tab_col);
8892 RenderNavHighlight(bb, id);
8893
8894 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
8895 const bool hovered_unblocked = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8896 if (hovered_unblocked && (IsMouseClicked(button: 1) || IsMouseReleased(button: 1)) && !is_tab_button)
8897 TabBarQueueFocus(tab_bar, tab);
8898
8899 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
8900 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
8901
8902 // Render tab label, process close button
8903 const ImGuiID close_button_id = p_open ? GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: docked_window ? docked_window->ID : id) : 0;
8904 bool just_closed;
8905 bool text_clipped;
8906 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);
8907 if (just_closed && p_open != NULL)
8908 {
8909 *p_open = false;
8910 TabBarCloseTab(tab_bar, tab);
8911 }
8912
8913 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
8914 // That state is copied to window->DockTabItemStatusFlags by our caller.
8915 if (docked_window && (hovered || g.HoveredId == close_button_id))
8916 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
8917
8918 // Restore main window position so user can draw there
8919 if (want_clip_rect)
8920 PopClipRect();
8921 window->DC.CursorPos = backup_main_cursor_pos;
8922
8923 // Tooltip
8924 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
8925 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
8926 // FIXME: This is a mess.
8927 // FIXME: We may want disabled tab to still display the tooltip?
8928 if (text_clipped && g.HoveredId == id && !held)
8929 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
8930 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(text: label) - label), label);
8931
8932 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
8933 if (is_tab_button)
8934 return pressed;
8935 return tab_contents_visible;
8936}
8937
8938// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
8939// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
8940// Tabs closed by the close button will automatically be flagged to avoid this issue.
8941void ImGui::SetTabItemClosed(const char* label)
8942{
8943 ImGuiContext& g = *GImGui;
8944 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
8945 if (is_within_manual_tab_bar)
8946 {
8947 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8948 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
8949 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
8950 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
8951 }
8952 else if (ImGuiWindow* window = FindWindowByName(name: label))
8953 {
8954 if (window->DockIsActive)
8955 if (ImGuiDockNode* node = window->DockNode)
8956 {
8957 ImGuiID tab_id = TabBarCalcTabID(tab_bar: node->TabBar, label, docked_window: window);
8958 TabBarRemoveTab(tab_bar: node->TabBar, tab_id);
8959 window->DockTabWantClose = true;
8960 }
8961 }
8962}
8963
8964ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
8965{
8966 ImGuiContext& g = *GImGui;
8967 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8968 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
8969 if (has_close_button_or_unsaved_marker)
8970 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
8971 else
8972 size.x += g.Style.FramePadding.x + 1.0f;
8973 return ImVec2(ImMin(lhs: size.x, rhs: TabBarCalcMaxTabWidth()), size.y);
8974}
8975
8976ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
8977{
8978 return TabItemCalcSize(label: window->Name, has_close_button_or_unsaved_marker: window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
8979}
8980
8981void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
8982{
8983 // 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.
8984 ImGuiContext& g = *GImGui;
8985 const float width = bb.GetWidth();
8986 IM_UNUSED(flags);
8987 IM_ASSERT(width > 0.0f);
8988 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));
8989 const float y1 = bb.Min.y + 1.0f;
8990 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
8991 draw_list->PathLineTo(pos: ImVec2(bb.Min.x, y2));
8992 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding, y1 + rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
8993 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding, y1 + rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
8994 draw_list->PathLineTo(pos: ImVec2(bb.Max.x, y2));
8995 draw_list->PathFillConvex(col);
8996 if (g.Style.TabBorderSize > 0.0f)
8997 {
8998 draw_list->PathLineTo(pos: ImVec2(bb.Min.x + 0.5f, y2));
8999 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);
9000 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);
9001 draw_list->PathLineTo(pos: ImVec2(bb.Max.x - 0.5f, y2));
9002 draw_list->PathStroke(col: GetColorU32(idx: ImGuiCol_Border), flags: 0, thickness: g.Style.TabBorderSize);
9003 }
9004}
9005
9006// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
9007// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
9008void 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)
9009{
9010 ImGuiContext& g = *GImGui;
9011 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
9012
9013 if (out_just_closed)
9014 *out_just_closed = false;
9015 if (out_text_clipped)
9016 *out_text_clipped = false;
9017
9018 if (bb.GetWidth() <= 1.0f)
9019 return;
9020
9021 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
9022 // But right now if you want to alter text color of tabs this is what you need to do.
9023#if 0
9024 const float backup_alpha = g.Style.Alpha;
9025 if (!is_contents_visible)
9026 g.Style.Alpha *= 0.7f;
9027#endif
9028
9029 // Render text label (with clipping + alpha gradient) + unsaved marker
9030 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);
9031 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
9032
9033 // Return clipped state ignoring the close button
9034 if (out_text_clipped)
9035 {
9036 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
9037 //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));
9038 }
9039
9040 const float button_sz = g.FontSize;
9041 const ImVec2 button_pos(ImMax(lhs: bb.Min.x, rhs: bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
9042
9043 // Close Button & Unsaved Marker
9044 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
9045 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
9046 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
9047 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
9048 bool close_button_pressed = false;
9049 bool close_button_visible = false;
9050 if (close_button_id != 0)
9051 if (is_contents_visible || bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabMinWidthForCloseButton))
9052 if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
9053 close_button_visible = true;
9054 bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
9055
9056 if (close_button_visible)
9057 {
9058 ImGuiLastItemData last_item_backup = g.LastItemData;
9059 if (CloseButton(id: close_button_id, pos: button_pos))
9060 close_button_pressed = true;
9061 g.LastItemData = last_item_backup;
9062
9063 // Close with middle mouse button
9064 if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(button: 2))
9065 close_button_pressed = true;
9066 }
9067 else if (unsaved_marker_visible)
9068 {
9069 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
9070 RenderBullet(draw_list, pos: bullet_bb.GetCenter(), col: GetColorU32(idx: ImGuiCol_Text));
9071 }
9072
9073 // This is all rather complicated
9074 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
9075 // 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..
9076 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
9077 if (close_button_visible || unsaved_marker_visible)
9078 {
9079 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
9080 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
9081 ellipsis_max_x = text_pixel_clip_bb.Max.x;
9082 }
9083 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);
9084
9085#if 0
9086 if (!is_contents_visible)
9087 g.Style.Alpha = backup_alpha;
9088#endif
9089
9090 if (out_just_closed)
9091 *out_just_closed = close_button_pressed;
9092}
9093
9094
9095#endif // #ifndef IMGUI_DISABLE
9096

source code of imgui/imgui_widgets.cpp