1// dear imgui, v1.89.2 WIP
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: ListBox
22// [SECTION] Widgets: PlotLines, PlotHistogram
23// [SECTION] Widgets: Value helpers
24// [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc.
25// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
26// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
27// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc.
28
29*/
30
31#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
32#define _CRT_SECURE_NO_WARNINGS
33#endif
34
35#include "imgui.h"
36#ifndef IMGUI_DISABLE
37
38#ifndef IMGUI_DEFINE_MATH_OPERATORS
39#define IMGUI_DEFINE_MATH_OPERATORS
40#endif
41#include "imgui_internal.h"
42
43// System includes
44#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier
45#include <stddef.h> // intptr_t
46#else
47#include <stdint.h> // intptr_t
48#endif
49
50//-------------------------------------------------------------------------
51// Warnings
52//-------------------------------------------------------------------------
53
54// Visual Studio warnings
55#ifdef _MSC_VER
56#pragma warning (disable: 4127) // condition expression is constant
57#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen
58#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later
59#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types
60#endif
61#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2).
62#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
63#endif
64
65// Clang/GCC warnings with -Weverything
66#if defined(__clang__)
67#if __has_warning("-Wunknown-warning-option")
68#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great!
69#endif
70#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx'
71#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
72#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok.
73#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code.
74#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
75#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
76#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.
77#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
78#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
79#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
80#elif defined(__GNUC__)
81#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
82#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
83#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
84#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
85#endif
86
87//-------------------------------------------------------------------------
88// Data
89//-------------------------------------------------------------------------
90
91// Widgets
92static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
93static 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.
94
95// Those MIN/MAX values are not define because we need to point to them
96static const signed char IM_S8_MIN = -128;
97static const signed char IM_S8_MAX = 127;
98static const unsigned char IM_U8_MIN = 0;
99static const unsigned char IM_U8_MAX = 0xFF;
100static const signed short IM_S16_MIN = -32768;
101static const signed short IM_S16_MAX = 32767;
102static const unsigned short IM_U16_MIN = 0;
103static const unsigned short IM_U16_MAX = 0xFFFF;
104static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
105static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
106static const ImU32 IM_U32_MIN = 0;
107static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
108#ifdef LLONG_MIN
109static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
110static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
111#else
112static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
113static const ImS64 IM_S64_MAX = 9223372036854775807LL;
114#endif
115static const ImU64 IM_U64_MIN = 0;
116#ifdef ULLONG_MAX
117static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
118#else
119static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
120#endif
121
122//-------------------------------------------------------------------------
123// [SECTION] Forward Declarations
124//-------------------------------------------------------------------------
125
126// For InputTextEx()
127static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source);
128static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
129static 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);
130
131//-------------------------------------------------------------------------
132// [SECTION] Widgets: Text, etc.
133//-------------------------------------------------------------------------
134// - TextEx() [Internal]
135// - TextUnformatted()
136// - Text()
137// - TextV()
138// - TextColored()
139// - TextColoredV()
140// - TextDisabled()
141// - TextDisabledV()
142// - TextWrapped()
143// - TextWrappedV()
144// - LabelText()
145// - LabelTextV()
146// - BulletText()
147// - BulletTextV()
148//-------------------------------------------------------------------------
149
150void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
151{
152 ImGuiWindow* window = GetCurrentWindow();
153 if (window->SkipItems)
154 return;
155 ImGuiContext& g = *GImGui;
156
157 // Accept null ranges
158 if (text == text_end)
159 text = text_end = "";
160
161 // Calculate length
162 const char* text_begin = text;
163 if (text_end == NULL)
164 text_end = text + strlen(s: text); // FIXME-OPT
165
166 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
167 const float wrap_pos_x = window->DC.TextWrapPos;
168 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
169 if (text_end - text <= 2000 || wrap_enabled)
170 {
171 // Common case
172 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(pos: window->DC.CursorPos, wrap_pos_x) : 0.0f;
173 const ImVec2 text_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false, wrap_width);
174
175 ImRect bb(text_pos, text_pos + text_size);
176 ItemSize(size: text_size, text_baseline_y: 0.0f);
177 if (!ItemAdd(bb, id: 0))
178 return;
179
180 // Render (we don't hide text after ## in this end-user function)
181 RenderTextWrapped(pos: bb.Min, text: text_begin, text_end, wrap_width);
182 }
183 else
184 {
185 // Long text!
186 // Perform manual coarse clipping to optimize for long multi-line text
187 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
188 // - 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.
189 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
190 const char* line = text;
191 const float line_height = GetTextLineHeight();
192 ImVec2 text_size(0, 0);
193
194 // Lines to skip (can't skip when logging text)
195 ImVec2 pos = text_pos;
196 if (!g.LogEnabled)
197 {
198 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
199 if (lines_skippable > 0)
200 {
201 int lines_skipped = 0;
202 while (line < text_end && lines_skipped < lines_skippable)
203 {
204 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
205 if (!line_end)
206 line_end = text_end;
207 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
208 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
209 line = line_end + 1;
210 lines_skipped++;
211 }
212 pos.y += lines_skipped * line_height;
213 }
214 }
215
216 // Lines to render
217 if (line < text_end)
218 {
219 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
220 while (line < text_end)
221 {
222 if (IsClippedEx(bb: line_rect, id: 0))
223 break;
224
225 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
226 if (!line_end)
227 line_end = text_end;
228 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
229 RenderText(pos, text: line, text_end: line_end, hide_text_after_hash: false);
230 line = line_end + 1;
231 line_rect.Min.y += line_height;
232 line_rect.Max.y += line_height;
233 pos.y += line_height;
234 }
235
236 // Count remaining lines
237 int lines_skipped = 0;
238 while (line < text_end)
239 {
240 const char* line_end = (const char*)memchr(s: line, c: '\n', n: text_end - line);
241 if (!line_end)
242 line_end = text_end;
243 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
244 text_size.x = ImMax(lhs: text_size.x, rhs: CalcTextSize(text: line, text_end: line_end).x);
245 line = line_end + 1;
246 lines_skipped++;
247 }
248 pos.y += lines_skipped * line_height;
249 }
250 text_size.y = (pos - text_pos).y;
251
252 ImRect bb(text_pos, text_pos + text_size);
253 ItemSize(size: text_size, text_baseline_y: 0.0f);
254 ItemAdd(bb, id: 0);
255 }
256}
257
258void ImGui::TextUnformatted(const char* text, const char* text_end)
259{
260 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
261}
262
263void ImGui::Text(const char* fmt, ...)
264{
265 va_list args;
266 va_start(args, fmt);
267 TextV(fmt, args);
268 va_end(args);
269}
270
271void ImGui::TextV(const char* fmt, va_list args)
272{
273 ImGuiWindow* window = GetCurrentWindow();
274 if (window->SkipItems)
275 return;
276
277 // FIXME-OPT: Handle the %s shortcut?
278 const char* text, *text_end;
279 ImFormatStringToTempBufferV(out_buf: &text, out_buf_end: &text_end, fmt, args);
280 TextEx(text, text_end, flags: ImGuiTextFlags_NoWidthForLargeClippedText);
281}
282
283void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
284{
285 va_list args;
286 va_start(args, fmt);
287 TextColoredV(col, fmt, args);
288 va_end(args);
289}
290
291void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
292{
293 PushStyleColor(idx: ImGuiCol_Text, col);
294 if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
295 TextEx(va_arg(args, const char*), NULL, flags: ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
296 else
297 TextV(fmt, args);
298 PopStyleColor();
299}
300
301void ImGui::TextDisabled(const char* fmt, ...)
302{
303 va_list args;
304 va_start(args, fmt);
305 TextDisabledV(fmt, args);
306 va_end(args);
307}
308
309void ImGui::TextDisabledV(const char* fmt, va_list args)
310{
311 ImGuiContext& g = *GImGui;
312 PushStyleColor(idx: ImGuiCol_Text, col: g.Style.Colors[ImGuiCol_TextDisabled]);
313 if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
314 TextEx(va_arg(args, const char*), NULL, flags: ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
315 else
316 TextV(fmt, args);
317 PopStyleColor();
318}
319
320void ImGui::TextWrapped(const char* fmt, ...)
321{
322 va_list args;
323 va_start(args, fmt);
324 TextWrappedV(fmt, args);
325 va_end(args);
326}
327
328void ImGui::TextWrappedV(const char* fmt, va_list args)
329{
330 ImGuiContext& g = *GImGui;
331 bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
332 if (need_backup)
333 PushTextWrapPos(wrap_local_pos_x: 0.0f);
334 if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0)
335 TextEx(va_arg(args, const char*), NULL, flags: ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting
336 else
337 TextV(fmt, args);
338 if (need_backup)
339 PopTextWrapPos();
340}
341
342void ImGui::LabelText(const char* label, const char* fmt, ...)
343{
344 va_list args;
345 va_start(args, fmt);
346 LabelTextV(label, fmt, args);
347 va_end(args);
348}
349
350// Add a label+text combo aligned to other label+value widgets
351void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
352{
353 ImGuiWindow* window = GetCurrentWindow();
354 if (window->SkipItems)
355 return;
356
357 ImGuiContext& g = *GImGui;
358 const ImGuiStyle& style = g.Style;
359 const float w = CalcItemWidth();
360
361 const char* value_text_begin, *value_text_end;
362 ImFormatStringToTempBufferV(out_buf: &value_text_begin, out_buf_end: &value_text_end, fmt, args);
363 const ImVec2 value_size = CalcTextSize(text: value_text_begin, text_end: value_text_end, hide_text_after_double_hash: false);
364 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
365
366 const ImVec2 pos = window->DC.CursorPos;
367 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
368 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(lhs: value_size.y, rhs: label_size.y) + style.FramePadding.y * 2));
369 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
370 if (!ItemAdd(bb: total_bb, id: 0))
371 return;
372
373 // Render
374 RenderTextClipped(pos_min: value_bb.Min + style.FramePadding, pos_max: value_bb.Max, text: value_text_begin, text_end: value_text_end, text_size_if_known: &value_size, align: ImVec2(0.0f, 0.0f));
375 if (label_size.x > 0.0f)
376 RenderText(pos: ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), text: label);
377}
378
379void ImGui::BulletText(const char* fmt, ...)
380{
381 va_list args;
382 va_start(args, fmt);
383 BulletTextV(fmt, args);
384 va_end(args);
385}
386
387// Text with a little bullet aligned to the typical tree node.
388void ImGui::BulletTextV(const char* fmt, va_list args)
389{
390 ImGuiWindow* window = GetCurrentWindow();
391 if (window->SkipItems)
392 return;
393
394 ImGuiContext& g = *GImGui;
395 const ImGuiStyle& style = g.Style;
396
397 const char* text_begin, *text_end;
398 ImFormatStringToTempBufferV(out_buf: &text_begin, out_buf_end: &text_end, fmt, args);
399 const ImVec2 label_size = CalcTextSize(text: text_begin, text_end, hide_text_after_double_hash: false);
400 const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding
401 ImVec2 pos = window->DC.CursorPos;
402 pos.y += window->DC.CurrLineTextBaseOffset;
403 ItemSize(size: total_size, text_baseline_y: 0.0f);
404 const ImRect bb(pos, pos + total_size);
405 if (!ItemAdd(bb, id: 0))
406 return;
407
408 // Render
409 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
410 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), col: text_col);
411 RenderText(pos: bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text: text_begin, text_end, hide_text_after_hash: false);
412}
413
414//-------------------------------------------------------------------------
415// [SECTION] Widgets: Main
416//-------------------------------------------------------------------------
417// - ButtonBehavior() [Internal]
418// - Button()
419// - SmallButton()
420// - InvisibleButton()
421// - ArrowButton()
422// - CloseButton() [Internal]
423// - CollapseButton() [Internal]
424// - GetWindowScrollbarID() [Internal]
425// - GetWindowScrollbarRect() [Internal]
426// - Scrollbar() [Internal]
427// - ScrollbarEx() [Internal]
428// - Image()
429// - ImageButton()
430// - Checkbox()
431// - CheckboxFlagsT() [Internal]
432// - CheckboxFlags()
433// - RadioButton()
434// - ProgressBar()
435// - Bullet()
436//-------------------------------------------------------------------------
437
438// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
439// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
440// this code is a little complex.
441// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
442// See the series of events below and the corresponding state reported by dear imgui:
443//------------------------------------------------------------------------------------------------------------------------------------------------
444// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
445// Frame N+0 (mouse is outside bb) - - - - - -
446// Frame N+1 (mouse moves inside bb) - true - - - -
447// Frame N+2 (mouse button is down) - true true true - true
448// Frame N+3 (mouse button is down) - true true - - -
449// Frame N+4 (mouse moves outside bb) - - true - - -
450// Frame N+5 (mouse moves inside bb) - true true - - -
451// Frame N+6 (mouse button is released) true true - - true -
452// Frame N+7 (mouse button is released) - true - - - -
453// Frame N+8 (mouse moves outside bb) - - - - - -
454//------------------------------------------------------------------------------------------------------------------------------------------------
455// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
456// Frame N+2 (mouse button is down) true true true true - true
457// Frame N+3 (mouse button is down) - true true - - -
458// Frame N+6 (mouse button is released) - true - - true -
459// Frame N+7 (mouse button is released) - true - - - -
460//------------------------------------------------------------------------------------------------------------------------------------------------
461// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
462// Frame N+2 (mouse button is down) - true - - - true
463// Frame N+3 (mouse button is down) - true - - - -
464// Frame N+6 (mouse button is released) true true - - - -
465// Frame N+7 (mouse button is released) - true - - - -
466//------------------------------------------------------------------------------------------------------------------------------------------------
467// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
468// Frame N+0 (mouse button is down) - true - - - true
469// Frame N+1 (mouse button is down) - true - - - -
470// Frame N+2 (mouse button is released) - true - - - -
471// Frame N+3 (mouse button is released) - true - - - -
472// Frame N+4 (mouse button is down) true true true true - true
473// Frame N+5 (mouse button is down) - true true - - -
474// Frame N+6 (mouse button is released) - true - - true -
475// Frame N+7 (mouse button is released) - true - - - -
476//------------------------------------------------------------------------------------------------------------------------------------------------
477// Note that some combinations are supported,
478// - PressedOnDragDropHold can generally be associated with any flag.
479// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
480//------------------------------------------------------------------------------------------------------------------------------------------------
481// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set:
482// Repeat+ Repeat+ Repeat+ Repeat+
483// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
484//-------------------------------------------------------------------------------------------------------------------------------------------------
485// Frame N+0 (mouse button is down) - true - true
486// ... - - - -
487// Frame N + RepeatDelay true true - true
488// ... - - - -
489// Frame N + RepeatDelay + RepeatRate*N true true - true
490//-------------------------------------------------------------------------------------------------------------------------------------------------
491
492bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
493{
494 ImGuiContext& g = *GImGui;
495 ImGuiWindow* window = GetCurrentWindow();
496
497 // Default only reacts to left mouse button
498 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
499 flags |= ImGuiButtonFlags_MouseButtonDefault_;
500
501 // Default behavior requires click + release inside bounding box
502 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
503 flags |= ImGuiButtonFlags_PressedOnDefault_;
504
505 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
506 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
507 if (flatten_hovered_children)
508 g.HoveredWindow = window;
509
510#ifdef IMGUI_ENABLE_TEST_ENGINE
511 if (id != 0 && g.LastItemData.ID != id)
512 IMGUI_TEST_ENGINE_ITEM_ADD(bb, id);
513#endif
514
515 bool pressed = false;
516 bool hovered = ItemHoverable(bb, id);
517
518 // Drag source doesn't report as hovered
519 if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover))
520 hovered = false;
521
522 // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
523 if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
524 if (IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
525 {
526 hovered = true;
527 SetHoveredID(id);
528 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
529 {
530 pressed = true;
531 g.DragDropHoldJustPressedId = id;
532 FocusWindow(window);
533 }
534 }
535
536 if (flatten_hovered_children)
537 g.HoveredWindow = backup_hovered_window;
538
539 // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one.
540 if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0))
541 hovered = false;
542
543 // Mouse handling
544 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
545 if (hovered)
546 {
547 if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt))
548 {
549 // Poll buttons
550 int mouse_button_clicked = -1;
551 if ((flags & ImGuiButtonFlags_MouseButtonLeft) && IsMouseClicked(button: 0, owner_id: test_owner_id)) { mouse_button_clicked = 0; }
552 else if ((flags & ImGuiButtonFlags_MouseButtonRight) && IsMouseClicked(button: 1, owner_id: test_owner_id)) { mouse_button_clicked = 1; }
553 else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && IsMouseClicked(button: 2, owner_id: test_owner_id)) { mouse_button_clicked = 2; }
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 int mouse_button_released = -1;
582 if ((flags & ImGuiButtonFlags_MouseButtonLeft) && IsMouseReleased(button: 0, owner_id: test_owner_id)) { mouse_button_released = 0; }
583 else if ((flags & ImGuiButtonFlags_MouseButtonRight) && IsMouseReleased(button: 1, owner_id: test_owner_id)) { mouse_button_released = 1; }
584 else if ((flags & ImGuiButtonFlags_MouseButtonMiddle) && IsMouseReleased(button: 2, owner_id: test_owner_id)) { mouse_button_released = 2; }
585 if (mouse_button_released != -1)
586 {
587 const bool has_repeated_at_least_once = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior
588 if (!has_repeated_at_least_once)
589 pressed = true;
590 if (!(flags & ImGuiButtonFlags_NoNavFocus))
591 SetFocusID(id, window);
592 ClearActiveID();
593 }
594 }
595
596 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
597 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
598 if (g.ActiveId == id && (flags & ImGuiButtonFlags_Repeat))
599 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(button: g.ActiveIdMouseButton, owner_id: test_owner_id, flags: ImGuiInputFlags_Repeat))
600 pressed = true;
601 }
602
603 if (pressed)
604 g.NavDisableHighlight = true;
605 }
606
607 // Gamepad/Keyboard navigation
608 // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse.
609 if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId))
610 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
611 hovered = true;
612 if (g.NavActivateDownId == id)
613 {
614 bool nav_activated_by_code = (g.NavActivateId == id);
615 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
616 if (!nav_activated_by_inputs && (flags & ImGuiButtonFlags_Repeat))
617 {
618 // Avoid pressing both keys from triggering double amount of repeat events
619 const ImGuiKeyData* key1 = GetKeyData(key: ImGuiKey_Space);
620 const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_NavGamepadActivate);
621 const float t1 = ImMax(lhs: key1->DownDuration, rhs: key2->DownDuration);
622 nav_activated_by_inputs = CalcTypematicRepeatAmount(t0: t1 - g.IO.DeltaTime, t1, repeat_delay: g.IO.KeyRepeatDelay, repeat_rate: g.IO.KeyRepeatRate) > 0;
623 }
624 if (nav_activated_by_code || nav_activated_by_inputs)
625 {
626 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
627 pressed = true;
628 SetActiveID(id, window);
629 g.ActiveIdSource = ImGuiInputSource_Nav;
630 if (!(flags & ImGuiButtonFlags_NoNavFocus))
631 SetFocusID(id, window);
632 }
633 }
634
635 // Process while held
636 bool held = false;
637 if (g.ActiveId == id)
638 {
639 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
640 {
641 if (g.ActiveIdIsJustActivated)
642 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
643
644 const int mouse_button = g.ActiveIdMouseButton;
645 if (IsMouseDown(button: mouse_button, owner_id: test_owner_id))
646 {
647 held = true;
648 }
649 else
650 {
651 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
652 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
653 if ((release_in || release_anywhere) && !g.DragDropActive)
654 {
655 // Report as pressed when releasing the mouse (this is the most common path)
656 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
657 bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
658 bool is_button_avail_or_owned = TestKeyOwner(key: MouseButtonToKey(button: mouse_button), owner_id: test_owner_id);
659 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
660 pressed = true;
661 }
662 ClearActiveID();
663 }
664 if (!(flags & ImGuiButtonFlags_NoNavFocus))
665 g.NavDisableHighlight = true;
666 }
667 else if (g.ActiveIdSource == ImGuiInputSource_Nav)
668 {
669 // When activated using Nav, we hold on the ActiveID until activation button is released
670 if (g.NavActivateDownId != id)
671 ClearActiveID();
672 }
673 if (pressed)
674 g.ActiveIdHasBeenPressedBefore = true;
675 }
676
677 if (out_hovered) *out_hovered = hovered;
678 if (out_held) *out_held = held;
679
680 return pressed;
681}
682
683bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
684{
685 ImGuiWindow* window = GetCurrentWindow();
686 if (window->SkipItems)
687 return false;
688
689 ImGuiContext& g = *GImGui;
690 const ImGuiStyle& style = g.Style;
691 const ImGuiID id = window->GetID(str: label);
692 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
693
694 ImVec2 pos = window->DC.CursorPos;
695 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)
696 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
697 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);
698
699 const ImRect bb(pos, pos + size);
700 ItemSize(size, text_baseline_y: style.FramePadding.y);
701 if (!ItemAdd(bb, id))
702 return false;
703
704 if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat)
705 flags |= ImGuiButtonFlags_Repeat;
706
707 bool hovered, held;
708 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
709
710 // Render
711 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
712 RenderNavHighlight(bb, id);
713 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: true, rounding: style.FrameRounding);
714
715 if (g.LogEnabled)
716 LogSetNextTextDecoration(prefix: "[", suffix: "]");
717 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);
718
719 // Automatically close popups
720 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
721 // CloseCurrentPopup();
722
723 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
724 return pressed;
725}
726
727bool ImGui::Button(const char* label, const ImVec2& size_arg)
728{
729 return ButtonEx(label, size_arg, flags: ImGuiButtonFlags_None);
730}
731
732// Small buttons fits within text without additional vertical spacing.
733bool ImGui::SmallButton(const char* label)
734{
735 ImGuiContext& g = *GImGui;
736 float backup_padding_y = g.Style.FramePadding.y;
737 g.Style.FramePadding.y = 0.0f;
738 bool pressed = ButtonEx(label, size_arg: ImVec2(0, 0), flags: ImGuiButtonFlags_AlignTextBaseLine);
739 g.Style.FramePadding.y = backup_padding_y;
740 return pressed;
741}
742
743// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
744// 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)
745bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
746{
747 ImGuiContext& g = *GImGui;
748 ImGuiWindow* window = GetCurrentWindow();
749 if (window->SkipItems)
750 return false;
751
752 // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
753 IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
754
755 const ImGuiID id = window->GetID(str: str_id);
756 ImVec2 size = CalcItemSize(size: size_arg, default_w: 0.0f, default_h: 0.0f);
757 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
758 ItemSize(size);
759 if (!ItemAdd(bb, id))
760 return false;
761
762 bool hovered, held;
763 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
764
765 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
766 return pressed;
767}
768
769bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
770{
771 ImGuiContext& g = *GImGui;
772 ImGuiWindow* window = GetCurrentWindow();
773 if (window->SkipItems)
774 return false;
775
776 const ImGuiID id = window->GetID(str: str_id);
777 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
778 const float default_size = GetFrameHeight();
779 ItemSize(size, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
780 if (!ItemAdd(bb, id))
781 return false;
782
783 if (g.LastItemData.InFlags & ImGuiItemFlags_ButtonRepeat)
784 flags |= ImGuiButtonFlags_Repeat;
785
786 bool hovered, held;
787 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags);
788
789 // Render
790 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
791 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
792 RenderNavHighlight(bb, id);
793 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: bg_col, border: true, rounding: g.Style.FrameRounding);
794 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);
795
796 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
797 return pressed;
798}
799
800bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
801{
802 float sz = GetFrameHeight();
803 return ArrowButtonEx(str_id, dir, size: ImVec2(sz, sz), flags: ImGuiButtonFlags_None);
804}
805
806// Button to close a window
807bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
808{
809 ImGuiContext& g = *GImGui;
810 ImGuiWindow* window = g.CurrentWindow;
811
812 // 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)
813 // 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?
814 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
815 ImRect bb_interact = bb;
816 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
817 if (area_to_visible_ratio < 1.5f)
818 bb_interact.Expand(amount: ImFloor(v: bb_interact.GetSize() * -0.25f));
819
820 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
821 // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible).
822 bool is_clipped = !ItemAdd(bb: bb_interact, id);
823
824 bool hovered, held;
825 bool pressed = ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held);
826 if (is_clipped)
827 return pressed;
828
829 // Render
830 // FIXME: Clarify this mess
831 ImU32 col = GetColorU32(idx: held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
832 ImVec2 center = bb.GetCenter();
833 if (hovered)
834 window->DrawList->AddCircleFilled(center, radius: ImMax(lhs: 2.0f, rhs: g.FontSize * 0.5f + 1.0f), col, num_segments: 12);
835
836 float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
837 ImU32 cross_col = GetColorU32(idx: ImGuiCol_Text);
838 center -= ImVec2(0.5f, 0.5f);
839 window->DrawList->AddLine(p1: center + ImVec2(+cross_extent, +cross_extent), p2: center + ImVec2(-cross_extent, -cross_extent), col: cross_col, thickness: 1.0f);
840 window->DrawList->AddLine(p1: center + ImVec2(+cross_extent, -cross_extent), p2: center + ImVec2(-cross_extent, +cross_extent), col: cross_col, thickness: 1.0f);
841
842 return pressed;
843}
844
845// The Collapse button also functions as a Dock Menu button.
846bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
847{
848 ImGuiContext& g = *GImGui;
849 ImGuiWindow* window = g.CurrentWindow;
850
851 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
852 ItemAdd(bb, id);
853 bool hovered, held;
854 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_None);
855
856 // Render
857 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
858 ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
859 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
860 if (hovered || held)
861 window->DrawList->AddCircleFilled(center: bb.GetCenter() + ImVec2(0,-0.5f), radius: g.FontSize * 0.5f + 1.0f, col: bg_col, num_segments: 12);
862
863 if (dock_node)
864 RenderArrowDockMenu(draw_list: window->DrawList, p_min: bb.Min + g.Style.FramePadding, sz: g.FontSize, col: text_col);
865 else
866 RenderArrow(draw_list: window->DrawList, pos: bb.Min + g.Style.FramePadding, col: text_col, dir: window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, scale: 1.0f);
867
868 // Switch to moving the window after mouse is moved beyond the initial drag threshold
869 if (IsItemActive() && IsMouseDragging(button: 0))
870 StartMouseMovingWindowOrNode(window, node: dock_node, undock_floating_node: true);
871
872 return pressed;
873}
874
875ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
876{
877 return window->GetID(str: axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
878}
879
880// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
881ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
882{
883 const ImRect outer_rect = window->Rect();
884 const ImRect inner_rect = window->InnerRect;
885 const float border_size = window->WindowBorderSize;
886 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
887 IM_ASSERT(scrollbar_size > 0.0f);
888 if (axis == ImGuiAxis_X)
889 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, outer_rect.Max.y);
890 else
891 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, inner_rect.Max.y);
892}
893
894void ImGui::Scrollbar(ImGuiAxis axis)
895{
896 ImGuiContext& g = *GImGui;
897 ImGuiWindow* window = g.CurrentWindow;
898 const ImGuiID id = GetWindowScrollbarID(window, axis);
899
900 // Calculate scrollbar bounding box
901 ImRect bb = GetWindowScrollbarRect(window, axis);
902 ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
903 if (axis == ImGuiAxis_X)
904 {
905 rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
906 if (!window->ScrollbarY)
907 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
908 }
909 else
910 {
911 if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
912 rounding_corners |= ImDrawFlags_RoundCornersTopRight;
913 if (!window->ScrollbarX)
914 rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
915 }
916 float size_avail = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
917 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
918 ImS64 scroll = (ImS64)window->Scroll[axis];
919 ScrollbarEx(bb, id, axis, p_scroll_v: &scroll, avail_v: (ImS64)size_avail, contents_v: (ImS64)size_contents, flags: rounding_corners);
920 window->Scroll[axis] = (float)scroll;
921}
922
923// Vertical/Horizontal scrollbar
924// The entire piece of code below is rather confusing because:
925// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
926// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
927// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
928// Still, the code should probably be made simpler..
929bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_avail_v, ImS64 size_contents_v, ImDrawFlags flags)
930{
931 ImGuiContext& g = *GImGui;
932 ImGuiWindow* window = g.CurrentWindow;
933 if (window->SkipItems)
934 return false;
935
936 const float bb_frame_width = bb_frame.GetWidth();
937 const float bb_frame_height = bb_frame.GetHeight();
938 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
939 return false;
940
941 // 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)
942 float alpha = 1.0f;
943 if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
944 alpha = ImSaturate(f: (bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
945 if (alpha <= 0.0f)
946 return false;
947
948 const ImGuiStyle& style = g.Style;
949 const bool allow_interaction = (alpha >= 1.0f);
950
951 ImRect bb = bb_frame;
952 bb.Expand(amount: ImVec2(-ImClamp(IM_FLOOR((bb_frame_width - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f), -ImClamp(IM_FLOOR((bb_frame_height - 2.0f) * 0.5f), mn: 0.0f, mx: 3.0f)));
953
954 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
955 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
956
957 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
958 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
959 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.
960 const ImS64 win_size_v = ImMax(lhs: ImMax(lhs: size_contents_v, rhs: size_avail_v), rhs: (ImS64)1);
961 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);
962 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
963
964 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
965 bool held = false;
966 bool hovered = false;
967 ItemAdd(bb: bb_frame, id, NULL, extra_flags: ImGuiItemFlags_NoNav);
968 ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_NoNavFocus);
969
970 const ImS64 scroll_max = ImMax(lhs: (ImS64)1, rhs: size_contents_v - size_avail_v);
971 float scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
972 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
973 if (held && allow_interaction && grab_h_norm < 1.0f)
974 {
975 const float scrollbar_pos_v = bb.Min[axis];
976 const float mouse_pos_v = g.IO.MousePos[axis];
977
978 // Click position in scrollbar normalized space (0.0f->1.0f)
979 const float clicked_v_norm = ImSaturate(f: (mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
980 SetHoveredID(id);
981
982 bool seek_absolute = false;
983 if (g.ActiveIdIsJustActivated)
984 {
985 // On initial click calculate the distance between mouse and the center of the grab
986 seek_absolute = (clicked_v_norm < grab_v_norm || clicked_v_norm > grab_v_norm + grab_h_norm);
987 if (seek_absolute)
988 g.ScrollbarClickDeltaToGrabCenter = 0.0f;
989 else
990 g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
991 }
992
993 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
994 // 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
995 const float scroll_v_norm = ImSaturate(f: (clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
996 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
997
998 // Update values for rendering
999 scroll_ratio = ImSaturate(f: (float)*p_scroll_v / (float)scroll_max);
1000 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1001
1002 // Update distance to grab now that we have seeked and saturated
1003 if (seek_absolute)
1004 g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1005 }
1006
1007 // Render
1008 const ImU32 bg_col = GetColorU32(idx: ImGuiCol_ScrollbarBg);
1009 const ImU32 grab_col = GetColorU32(idx: held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha_mul: alpha);
1010 window->DrawList->AddRectFilled(p_min: bb_frame.Min, p_max: bb_frame.Max, col: bg_col, rounding: window->WindowRounding, flags);
1011 ImRect grab_rect;
1012 if (axis == ImGuiAxis_X)
1013 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);
1014 else
1015 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);
1016 window->DrawList->AddRectFilled(p_min: grab_rect.Min, p_max: grab_rect.Max, col: grab_col, rounding: style.ScrollbarRounding);
1017
1018 return held;
1019}
1020
1021void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1022{
1023 ImGuiWindow* window = GetCurrentWindow();
1024 if (window->SkipItems)
1025 return;
1026
1027 ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1028 if (border_col.w > 0.0f)
1029 bb.Max += ImVec2(2, 2);
1030 ItemSize(bb);
1031 if (!ItemAdd(bb, id: 0))
1032 return;
1033
1034 if (border_col.w > 0.0f)
1035 {
1036 window->DrawList->AddRect(p_min: bb.Min, p_max: bb.Max, col: GetColorU32(col: border_col), rounding: 0.0f);
1037 window->DrawList->AddImage(user_texture_id, p_min: bb.Min + ImVec2(1, 1), p_max: bb.Max - ImVec2(1, 1), uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1038 }
1039 else
1040 {
1041 window->DrawList->AddImage(user_texture_id, p_min: bb.Min, p_max: bb.Max, uv_min: uv0, uv_max: uv1, col: GetColorU32(col: tint_col));
1042 }
1043}
1044
1045// ImageButton() is flawed as 'id' is always derived from 'texture_id' (see #2464 #1390)
1046// We provide this internal helper to write your own variant while we figure out how to redesign the public ImageButton() API.
1047bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1048{
1049 ImGuiContext& g = *GImGui;
1050 ImGuiWindow* window = GetCurrentWindow();
1051 if (window->SkipItems)
1052 return false;
1053
1054 const ImVec2 padding = g.Style.FramePadding;
1055 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2.0f);
1056 ItemSize(bb);
1057 if (!ItemAdd(bb, id))
1058 return false;
1059
1060 bool hovered, held;
1061 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1062
1063 // Render
1064 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1065 RenderNavHighlight(bb, id);
1066 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));
1067 if (bg_col.w > 0.0f)
1068 window->DrawList->AddRectFilled(p_min: bb.Min + padding, p_max: bb.Max - padding, col: GetColorU32(col: bg_col));
1069 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));
1070
1071 return pressed;
1072}
1073
1074bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1075{
1076 ImGuiContext& g = *GImGui;
1077 ImGuiWindow* window = g.CurrentWindow;
1078 if (window->SkipItems)
1079 return false;
1080
1081 return ImageButtonEx(id: window->GetID(str: str_id), texture_id: user_texture_id, size, uv0, uv1, bg_col, tint_col);
1082}
1083
1084#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1085// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1086// - 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)
1087// - new ImageButton() always use style.FramePadding Old ImageButton() had an override argument.
1088// If you need to change padding with new ImageButton() you can use PushStyleVar(ImGuiStyleVar_FramePadding, value), consistent with other Button functions.
1089bool 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)
1090{
1091 ImGuiContext& g = *GImGui;
1092 ImGuiWindow* window = g.CurrentWindow;
1093 if (window->SkipItems)
1094 return false;
1095
1096 // Default to using texture ID as ID. User can still push string/integer prefixes.
1097 PushID(ptr_id: (void*)(intptr_t)user_texture_id);
1098 const ImGuiID id = window->GetID(str: "#image");
1099 PopID();
1100
1101 if (frame_padding >= 0)
1102 PushStyleVar(idx: ImGuiStyleVar_FramePadding, val: ImVec2((float)frame_padding, (float)frame_padding));
1103 bool ret = ImageButtonEx(id, texture_id: user_texture_id, size, uv0, uv1, bg_col, tint_col);
1104 if (frame_padding >= 0)
1105 PopStyleVar();
1106 return ret;
1107}
1108#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1109
1110bool ImGui::Checkbox(const char* label, bool* v)
1111{
1112 ImGuiWindow* window = GetCurrentWindow();
1113 if (window->SkipItems)
1114 return false;
1115
1116 ImGuiContext& g = *GImGui;
1117 const ImGuiStyle& style = g.Style;
1118 const ImGuiID id = window->GetID(str: label);
1119 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1120
1121 const float square_sz = GetFrameHeight();
1122 const ImVec2 pos = window->DC.CursorPos;
1123 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));
1124 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1125 if (!ItemAdd(bb: total_bb, id))
1126 {
1127 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1128 return false;
1129 }
1130
1131 bool hovered, held;
1132 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1133 if (pressed)
1134 {
1135 *v = !(*v);
1136 MarkItemEdited(id);
1137 }
1138
1139 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1140 RenderNavHighlight(bb: total_bb, id);
1141 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);
1142 ImU32 check_col = GetColorU32(idx: ImGuiCol_CheckMark);
1143 bool mixed_value = (g.LastItemData.InFlags & ImGuiItemFlags_MixedValue) != 0;
1144 if (mixed_value)
1145 {
1146 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1147 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1148 ImVec2 pad(ImMax(lhs: 1.0f, IM_FLOOR(square_sz / 3.6f)), ImMax(lhs: 1.0f, IM_FLOOR(square_sz / 3.6f)));
1149 window->DrawList->AddRectFilled(p_min: check_bb.Min + pad, p_max: check_bb.Max - pad, col: check_col, rounding: style.FrameRounding);
1150 }
1151 else if (*v)
1152 {
1153 const float pad = ImMax(lhs: 1.0f, IM_FLOOR(square_sz / 6.0f));
1154 RenderCheckMark(draw_list: window->DrawList, pos: check_bb.Min + ImVec2(pad, pad), col: check_col, sz: square_sz - pad * 2.0f);
1155 }
1156
1157 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1158 if (g.LogEnabled)
1159 LogRenderedText(ref_pos: &label_pos, text: mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1160 if (label_size.x > 0.0f)
1161 RenderText(pos: label_pos, text: label);
1162
1163 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1164 return pressed;
1165}
1166
1167template<typename T>
1168bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1169{
1170 bool all_on = (*flags & flags_value) == flags_value;
1171 bool any_on = (*flags & flags_value) != 0;
1172 bool pressed;
1173 if (!all_on && any_on)
1174 {
1175 ImGuiContext& g = *GImGui;
1176 ImGuiItemFlags backup_item_flags = g.CurrentItemFlags;
1177 g.CurrentItemFlags |= ImGuiItemFlags_MixedValue;
1178 pressed = Checkbox(label, v: &all_on);
1179 g.CurrentItemFlags = backup_item_flags;
1180 }
1181 else
1182 {
1183 pressed = Checkbox(label, v: &all_on);
1184
1185 }
1186 if (pressed)
1187 {
1188 if (all_on)
1189 *flags |= flags_value;
1190 else
1191 *flags &= ~flags_value;
1192 }
1193 return pressed;
1194}
1195
1196bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1197{
1198 return CheckboxFlagsT(label, flags, flags_value);
1199}
1200
1201bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1202{
1203 return CheckboxFlagsT(label, flags, flags_value);
1204}
1205
1206bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1207{
1208 return CheckboxFlagsT(label, flags, flags_value);
1209}
1210
1211bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1212{
1213 return CheckboxFlagsT(label, flags, flags_value);
1214}
1215
1216bool ImGui::RadioButton(const char* label, bool active)
1217{
1218 ImGuiWindow* window = GetCurrentWindow();
1219 if (window->SkipItems)
1220 return false;
1221
1222 ImGuiContext& g = *GImGui;
1223 const ImGuiStyle& style = g.Style;
1224 const ImGuiID id = window->GetID(str: label);
1225 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1226
1227 const float square_sz = GetFrameHeight();
1228 const ImVec2 pos = window->DC.CursorPos;
1229 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1230 const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f));
1231 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1232 if (!ItemAdd(bb: total_bb, id))
1233 return false;
1234
1235 ImVec2 center = check_bb.GetCenter();
1236 center.x = IM_ROUND(center.x);
1237 center.y = IM_ROUND(center.y);
1238 const float radius = (square_sz - 1.0f) * 0.5f;
1239
1240 bool hovered, held;
1241 bool pressed = ButtonBehavior(bb: total_bb, id, out_hovered: &hovered, out_held: &held);
1242 if (pressed)
1243 MarkItemEdited(id);
1244
1245 RenderNavHighlight(bb: total_bb, id);
1246 window->DrawList->AddCircleFilled(center, radius, col: GetColorU32(idx: (held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segments: 16);
1247 if (active)
1248 {
1249 const float pad = ImMax(lhs: 1.0f, IM_FLOOR(square_sz / 6.0f));
1250 window->DrawList->AddCircleFilled(center, radius: radius - pad, col: GetColorU32(idx: ImGuiCol_CheckMark), num_segments: 16);
1251 }
1252
1253 if (style.FrameBorderSize > 0.0f)
1254 {
1255 window->DrawList->AddCircle(center: center + ImVec2(1, 1), radius, col: GetColorU32(idx: ImGuiCol_BorderShadow), num_segments: 16, thickness: style.FrameBorderSize);
1256 window->DrawList->AddCircle(center, radius, col: GetColorU32(idx: ImGuiCol_Border), num_segments: 16, thickness: style.FrameBorderSize);
1257 }
1258
1259 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1260 if (g.LogEnabled)
1261 LogRenderedText(ref_pos: &label_pos, text: active ? "(x)" : "( )");
1262 if (label_size.x > 0.0f)
1263 RenderText(pos: label_pos, text: label);
1264
1265 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1266 return pressed;
1267}
1268
1269// 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..
1270bool ImGui::RadioButton(const char* label, int* v, int v_button)
1271{
1272 const bool pressed = RadioButton(label, active: *v == v_button);
1273 if (pressed)
1274 *v = v_button;
1275 return pressed;
1276}
1277
1278// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1279void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1280{
1281 ImGuiWindow* window = GetCurrentWindow();
1282 if (window->SkipItems)
1283 return;
1284
1285 ImGuiContext& g = *GImGui;
1286 const ImGuiStyle& style = g.Style;
1287
1288 ImVec2 pos = window->DC.CursorPos;
1289 ImVec2 size = CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: g.FontSize + style.FramePadding.y * 2.0f);
1290 ImRect bb(pos, pos + size);
1291 ItemSize(size, text_baseline_y: style.FramePadding.y);
1292 if (!ItemAdd(bb, id: 0))
1293 return;
1294
1295 // Render
1296 fraction = ImSaturate(f: fraction);
1297 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
1298 bb.Expand(amount: ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1299 const ImVec2 fill_br = ImVec2(ImLerp(a: bb.Min.x, b: bb.Max.x, t: fraction), bb.Max.y);
1300 RenderRectFilledRangeH(draw_list: window->DrawList, rect: bb, col: GetColorU32(idx: ImGuiCol_PlotHistogram), x_start_norm: 0.0f, x_end_norm: fraction, rounding: style.FrameRounding);
1301
1302 // Default displaying the fraction as percentage string, but user can override it
1303 char overlay_buf[32];
1304 if (!overlay)
1305 {
1306 ImFormatString(buf: overlay_buf, IM_ARRAYSIZE(overlay_buf), fmt: "%.0f%%", fraction * 100 + 0.01f);
1307 overlay = overlay_buf;
1308 }
1309
1310 ImVec2 overlay_size = CalcTextSize(text: overlay, NULL);
1311 if (overlay_size.x > 0.0f)
1312 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);
1313}
1314
1315void ImGui::Bullet()
1316{
1317 ImGuiWindow* window = GetCurrentWindow();
1318 if (window->SkipItems)
1319 return;
1320
1321 ImGuiContext& g = *GImGui;
1322 const ImGuiStyle& style = g.Style;
1323 const float line_height = ImMax(lhs: ImMin(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + style.FramePadding.y * 2), rhs: g.FontSize);
1324 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1325 ItemSize(bb);
1326 if (!ItemAdd(bb, id: 0))
1327 {
1328 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2);
1329 return;
1330 }
1331
1332 // Render and stay on same line
1333 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1334 RenderBullet(draw_list: window->DrawList, pos: bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), col: text_col);
1335 SameLine(offset_from_start_x: 0, spacing: style.FramePadding.x * 2.0f);
1336}
1337
1338//-------------------------------------------------------------------------
1339// [SECTION] Widgets: Low-level Layout helpers
1340//-------------------------------------------------------------------------
1341// - Spacing()
1342// - Dummy()
1343// - NewLine()
1344// - AlignTextToFramePadding()
1345// - SeparatorEx() [Internal]
1346// - Separator()
1347// - SplitterBehavior() [Internal]
1348// - ShrinkWidths() [Internal]
1349//-------------------------------------------------------------------------
1350
1351void ImGui::Spacing()
1352{
1353 ImGuiWindow* window = GetCurrentWindow();
1354 if (window->SkipItems)
1355 return;
1356 ItemSize(size: ImVec2(0, 0));
1357}
1358
1359void ImGui::Dummy(const ImVec2& size)
1360{
1361 ImGuiWindow* window = GetCurrentWindow();
1362 if (window->SkipItems)
1363 return;
1364
1365 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1366 ItemSize(size);
1367 ItemAdd(bb, id: 0);
1368}
1369
1370void ImGui::NewLine()
1371{
1372 ImGuiWindow* window = GetCurrentWindow();
1373 if (window->SkipItems)
1374 return;
1375
1376 ImGuiContext& g = *GImGui;
1377 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1378 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1379 window->DC.IsSameLine = false;
1380 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.
1381 ItemSize(size: ImVec2(0, 0));
1382 else
1383 ItemSize(size: ImVec2(0.0f, g.FontSize));
1384 window->DC.LayoutType = backup_layout_type;
1385}
1386
1387void ImGui::AlignTextToFramePadding()
1388{
1389 ImGuiWindow* window = GetCurrentWindow();
1390 if (window->SkipItems)
1391 return;
1392
1393 ImGuiContext& g = *GImGui;
1394 window->DC.CurrLineSize.y = ImMax(lhs: window->DC.CurrLineSize.y, rhs: g.FontSize + g.Style.FramePadding.y * 2);
1395 window->DC.CurrLineTextBaseOffset = ImMax(lhs: window->DC.CurrLineTextBaseOffset, rhs: g.Style.FramePadding.y);
1396}
1397
1398// Horizontal/vertical separating line
1399void ImGui::SeparatorEx(ImGuiSeparatorFlags flags)
1400{
1401 ImGuiWindow* window = GetCurrentWindow();
1402 if (window->SkipItems)
1403 return;
1404
1405 ImGuiContext& g = *GImGui;
1406 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1407
1408 float thickness_draw = 1.0f;
1409 float thickness_layout = 0.0f;
1410 if (flags & ImGuiSeparatorFlags_Vertical)
1411 {
1412 // Vertical separator, for menu bars (use current line height). Not exposed because it is misleading and it doesn't have an effect on regular layout.
1413 float y1 = window->DC.CursorPos.y;
1414 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1415 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness_draw, y2));
1416 ItemSize(size: ImVec2(thickness_layout, 0.0f));
1417 if (!ItemAdd(bb, id: 0))
1418 return;
1419
1420 // Draw
1421 window->DrawList->AddLine(p1: ImVec2(bb.Min.x, bb.Min.y), p2: ImVec2(bb.Min.x, bb.Max.y), col: GetColorU32(idx: ImGuiCol_Separator));
1422 if (g.LogEnabled)
1423 LogText(fmt: " |");
1424 }
1425 else if (flags & ImGuiSeparatorFlags_Horizontal)
1426 {
1427 // Horizontal Separator
1428 float x1 = window->Pos.x;
1429 float x2 = window->Pos.x + window->Size.x;
1430
1431 // FIXME-WORKRECT: old hack (#205) until we decide of consistent behavior with WorkRect/Indent and Separator
1432 if (g.GroupStack.Size > 0 && g.GroupStack.back().WindowID == window->ID)
1433 x1 += window->DC.Indent.x;
1434
1435 // FIXME-WORKRECT: In theory we should simply be using WorkRect.Min.x/Max.x everywhere but it isn't aesthetically what we want,
1436 // need to introduce a variant of WorkRect for that purpose. (#4787)
1437 if (ImGuiTable* table = g.CurrentTable)
1438 {
1439 x1 = table->Columns[table->CurrentColumn].MinX;
1440 x2 = table->Columns[table->CurrentColumn].MaxX;
1441 }
1442
1443 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1444 if (columns)
1445 PushColumnsBackground();
1446
1447 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1448 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1449 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness_draw));
1450 ItemSize(size: ImVec2(0.0f, thickness_layout));
1451 const bool item_visible = ItemAdd(bb, id: 0);
1452 if (item_visible)
1453 {
1454 // Draw
1455 window->DrawList->AddLine(p1: bb.Min, p2: ImVec2(bb.Max.x, bb.Min.y), col: GetColorU32(idx: ImGuiCol_Separator));
1456 if (g.LogEnabled)
1457 LogRenderedText(ref_pos: &bb.Min, text: "--------------------------------\n");
1458
1459 }
1460 if (columns)
1461 {
1462 PopColumnsBackground();
1463 columns->LineMinY = window->DC.CursorPos.y;
1464 }
1465 }
1466}
1467
1468void ImGui::Separator()
1469{
1470 ImGuiContext& g = *GImGui;
1471 ImGuiWindow* window = g.CurrentWindow;
1472 if (window->SkipItems)
1473 return;
1474
1475 // Those flags should eventually be overridable by the user
1476 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1477 flags |= ImGuiSeparatorFlags_SpanAllColumns; // NB: this only applies to legacy Columns() api as they relied on Separator() a lot.
1478 SeparatorEx(flags);
1479}
1480
1481// 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.
1482bool 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)
1483{
1484 ImGuiContext& g = *GImGui;
1485 ImGuiWindow* window = g.CurrentWindow;
1486
1487 if (!ItemAdd(bb, id, NULL, extra_flags: ImGuiItemFlags_NoNav))
1488 return false;
1489
1490 bool hovered, held;
1491 ImRect bb_interact = bb;
1492 bb_interact.Expand(amount: axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1493 ButtonBehavior(bb: bb_interact, id, out_hovered: &hovered, out_held: &held, flags: ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap);
1494 if (hovered)
1495 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1496 if (g.ActiveId != id)
1497 SetItemAllowOverlap();
1498
1499 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1500 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1501
1502 ImRect bb_render = bb;
1503 if (held)
1504 {
1505 ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min;
1506 float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x;
1507
1508 // Minimum pane size
1509 float size_1_maximum_delta = ImMax(lhs: 0.0f, rhs: *size1 - min_size1);
1510 float size_2_maximum_delta = ImMax(lhs: 0.0f, rhs: *size2 - min_size2);
1511 if (mouse_delta < -size_1_maximum_delta)
1512 mouse_delta = -size_1_maximum_delta;
1513 if (mouse_delta > size_2_maximum_delta)
1514 mouse_delta = size_2_maximum_delta;
1515
1516 // Apply resize
1517 if (mouse_delta != 0.0f)
1518 {
1519 if (mouse_delta < 0.0f)
1520 IM_ASSERT(*size1 + mouse_delta >= min_size1);
1521 if (mouse_delta > 0.0f)
1522 IM_ASSERT(*size2 - mouse_delta >= min_size2);
1523 *size1 += mouse_delta;
1524 *size2 -= mouse_delta;
1525 bb_render.Translate(d: (axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1526 MarkItemEdited(id);
1527 }
1528 }
1529
1530 // Render at new position
1531 if (bg_col & IM_COL32_A_MASK)
1532 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col: bg_col, rounding: 0.0f);
1533 const ImU32 col = GetColorU32(idx: held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1534 window->DrawList->AddRectFilled(p_min: bb_render.Min, p_max: bb_render.Max, col, rounding: 0.0f);
1535
1536 return held;
1537}
1538
1539static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1540{
1541 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1542 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1543 if (int d = (int)(b->Width - a->Width))
1544 return d;
1545 return (b->Index - a->Index);
1546}
1547
1548// Shrink excess width from a set of item, by removing width from the larger items first.
1549// Set items Width to -1.0f to disable shrinking this item.
1550void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1551{
1552 if (count == 1)
1553 {
1554 if (items[0].Width >= 0.0f)
1555 items[0].Width = ImMax(lhs: items[0].Width - width_excess, rhs: 1.0f);
1556 return;
1557 }
1558 ImQsort(base: items, count: (size_t)count, size_of_element: sizeof(ImGuiShrinkWidthItem), compare_func: ShrinkWidthItemComparer);
1559 int count_same_width = 1;
1560 while (width_excess > 0.0f && count_same_width < count)
1561 {
1562 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1563 count_same_width++;
1564 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);
1565 if (max_width_to_remove_per_item <= 0.0f)
1566 break;
1567 float width_to_remove_per_item = ImMin(lhs: width_excess / count_same_width, rhs: max_width_to_remove_per_item);
1568 for (int item_n = 0; item_n < count_same_width; item_n++)
1569 items[item_n].Width -= width_to_remove_per_item;
1570 width_excess -= width_to_remove_per_item * count_same_width;
1571 }
1572
1573 // Round width and redistribute remainder
1574 // 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.
1575 width_excess = 0.0f;
1576 for (int n = 0; n < count; n++)
1577 {
1578 float width_rounded = ImFloor(f: items[n].Width);
1579 width_excess += items[n].Width - width_rounded;
1580 items[n].Width = width_rounded;
1581 }
1582 while (width_excess > 0.0f)
1583 for (int n = 0; n < count && width_excess > 0.0f; n++)
1584 {
1585 float width_to_add = ImMin(lhs: items[n].InitialWidth - items[n].Width, rhs: 1.0f);
1586 items[n].Width += width_to_add;
1587 width_excess -= width_to_add;
1588 }
1589}
1590
1591//-------------------------------------------------------------------------
1592// [SECTION] Widgets: ComboBox
1593//-------------------------------------------------------------------------
1594// - CalcMaxPopupHeightFromItemCount() [Internal]
1595// - BeginCombo()
1596// - BeginComboPopup() [Internal]
1597// - EndCombo()
1598// - BeginComboPreview() [Internal]
1599// - EndComboPreview() [Internal]
1600// - Combo()
1601//-------------------------------------------------------------------------
1602
1603static float CalcMaxPopupHeightFromItemCount(int items_count)
1604{
1605 ImGuiContext& g = *GImGui;
1606 if (items_count <= 0)
1607 return FLT_MAX;
1608 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1609}
1610
1611bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1612{
1613 ImGuiContext& g = *GImGui;
1614 ImGuiWindow* window = GetCurrentWindow();
1615
1616 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
1617 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1618 if (window->SkipItems)
1619 return false;
1620
1621 const ImGuiStyle& style = g.Style;
1622 const ImGuiID id = window->GetID(str: label);
1623 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1624
1625 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1626 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
1627 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth();
1628 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1629 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1630 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
1631 if (!ItemAdd(bb: total_bb, id, nav_bb: &bb))
1632 return false;
1633
1634 // Open on click
1635 bool hovered, held;
1636 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
1637 const ImGuiID popup_id = ImHashStr(data: "##ComboPopup", data_size: 0, seed: id);
1638 bool popup_open = IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1639 if (pressed && !popup_open)
1640 {
1641 OpenPopupEx(id: popup_id, popup_flags: ImGuiPopupFlags_None);
1642 popup_open = true;
1643 }
1644
1645 // Render shape
1646 const ImU32 frame_col = GetColorU32(idx: hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1647 const float value_x2 = ImMax(lhs: bb.Min.x, rhs: bb.Max.x - arrow_size);
1648 RenderNavHighlight(bb, id);
1649 if (!(flags & ImGuiComboFlags_NoPreview))
1650 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);
1651 if (!(flags & ImGuiComboFlags_NoArrowButton))
1652 {
1653 ImU32 bg_col = GetColorU32(idx: (popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1654 ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
1655 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);
1656 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1657 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);
1658 }
1659 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding: style.FrameRounding);
1660
1661 // Custom preview
1662 if (flags & ImGuiComboFlags_CustomPreview)
1663 {
1664 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1665 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1666 preview_value = NULL;
1667 }
1668
1669 // Render preview and label
1670 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1671 {
1672 if (g.LogEnabled)
1673 LogSetNextTextDecoration(prefix: "{", suffix: "}");
1674 RenderTextClipped(pos_min: bb.Min + style.FramePadding, pos_max: ImVec2(value_x2, bb.Max.y), text: preview_value, NULL, NULL);
1675 }
1676 if (label_size.x > 0)
1677 RenderText(pos: ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), text: label);
1678
1679 if (!popup_open)
1680 return false;
1681
1682 g.NextWindowData.Flags = backup_next_window_data_flags;
1683 return BeginComboPopup(popup_id, bb, flags);
1684}
1685
1686bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1687{
1688 ImGuiContext& g = *GImGui;
1689 if (!IsPopupOpen(id: popup_id, popup_flags: ImGuiPopupFlags_None))
1690 {
1691 g.NextWindowData.ClearFlags();
1692 return false;
1693 }
1694
1695 // Set popup size
1696 float w = bb.GetWidth();
1697 if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1698 {
1699 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(lhs: g.NextWindowData.SizeConstraintRect.Min.x, rhs: w);
1700 }
1701 else
1702 {
1703 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1704 flags |= ImGuiComboFlags_HeightRegular;
1705 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1706 int popup_max_height_in_items = -1;
1707 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1708 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1709 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1710 SetNextWindowSizeConstraints(size_min: ImVec2(w, 0.0f), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
1711 }
1712
1713 // This is essentially a specialized version of BeginPopupEx()
1714 char name[16];
1715 ImFormatString(buf: name, IM_ARRAYSIZE(name), fmt: "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth
1716
1717 // Set position given a custom constraint (peak into expected window size so we can position it)
1718 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1719 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1720 if (ImGuiWindow* popup_window = FindWindowByName(name))
1721 if (popup_window->WasActive)
1722 {
1723 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1724 ImVec2 size_expected = CalcWindowNextAutoFitSize(window: popup_window);
1725 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1726 ImRect r_outer = GetPopupAllowedExtentRect(window: popup_window);
1727 ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos: bb.GetBL(), size: size_expected, last_dir: &popup_window->AutoPosLastDirection, r_outer, r_avoid: bb, policy: ImGuiPopupPositionPolicy_ComboBox);
1728 SetNextWindowPos(pos);
1729 }
1730
1731 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1732 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1733 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text
1734 bool ret = Begin(name, NULL, flags: window_flags);
1735 PopStyleVar();
1736 if (!ret)
1737 {
1738 EndPopup();
1739 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1740 return false;
1741 }
1742 return true;
1743}
1744
1745void ImGui::EndCombo()
1746{
1747 EndPopup();
1748}
1749
1750// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
1751// (Experimental, see GitHub issues: #1658, #4168)
1752bool ImGui::BeginComboPreview()
1753{
1754 ImGuiContext& g = *GImGui;
1755 ImGuiWindow* window = g.CurrentWindow;
1756 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1757
1758 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
1759 return false;
1760 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?
1761 if (!window->ClipRect.Contains(r: preview_data->PreviewRect)) // Narrower test (optional)
1762 return false;
1763
1764 // FIXME: This could be contained in a PushWorkRect() api
1765 preview_data->BackupCursorPos = window->DC.CursorPos;
1766 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
1767 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
1768 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
1769 preview_data->BackupLayout = window->DC.LayoutType;
1770 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
1771 window->DC.CursorMaxPos = window->DC.CursorPos;
1772 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
1773 window->DC.IsSameLine = false;
1774 PushClipRect(clip_rect_min: preview_data->PreviewRect.Min, clip_rect_max: preview_data->PreviewRect.Max, intersect_with_current_clip_rect: true);
1775
1776 return true;
1777}
1778
1779void ImGui::EndComboPreview()
1780{
1781 ImGuiContext& g = *GImGui;
1782 ImGuiWindow* window = g.CurrentWindow;
1783 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1784
1785 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
1786 ImDrawList* draw_list = window->DrawList;
1787 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
1788 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
1789 {
1790 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
1791 draw_list->_TryMergeDrawCmds();
1792 }
1793 PopClipRect();
1794 window->DC.CursorPos = preview_data->BackupCursorPos;
1795 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: preview_data->BackupCursorMaxPos);
1796 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
1797 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
1798 window->DC.LayoutType = preview_data->BackupLayout;
1799 window->DC.IsSameLine = false;
1800 preview_data->PreviewRect = ImRect();
1801}
1802
1803// Getter for the old Combo() API: const char*[]
1804static bool Items_ArrayGetter(void* data, int idx, const char** out_text)
1805{
1806 const char* const* items = (const char* const*)data;
1807 if (out_text)
1808 *out_text = items[idx];
1809 return true;
1810}
1811
1812// Getter for the old Combo() API: "item1\0item2\0item3\0"
1813static bool Items_SingleStringGetter(void* data, int idx, const char** out_text)
1814{
1815 // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited.
1816 const char* items_separated_by_zeros = (const char*)data;
1817 int items_count = 0;
1818 const char* p = items_separated_by_zeros;
1819 while (*p)
1820 {
1821 if (idx == items_count)
1822 break;
1823 p += strlen(s: p) + 1;
1824 items_count++;
1825 }
1826 if (!*p)
1827 return false;
1828 if (out_text)
1829 *out_text = p;
1830 return true;
1831}
1832
1833// Old API, prefer using BeginCombo() nowadays if you can.
1834bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items)
1835{
1836 ImGuiContext& g = *GImGui;
1837
1838 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
1839 const char* preview_value = NULL;
1840 if (*current_item >= 0 && *current_item < items_count)
1841 items_getter(data, *current_item, &preview_value);
1842
1843 // 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.
1844 if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
1845 SetNextWindowSizeConstraints(size_min: ImVec2(0, 0), size_max: ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(items_count: popup_max_height_in_items)));
1846
1847 if (!BeginCombo(label, preview_value, flags: ImGuiComboFlags_None))
1848 return false;
1849
1850 // Display items
1851 // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
1852 bool value_changed = false;
1853 for (int i = 0; i < items_count; i++)
1854 {
1855 PushID(int_id: i);
1856 const bool item_selected = (i == *current_item);
1857 const char* item_text;
1858 if (!items_getter(data, i, &item_text))
1859 item_text = "*Unknown item*";
1860 if (Selectable(label: item_text, selected: item_selected))
1861 {
1862 value_changed = true;
1863 *current_item = i;
1864 }
1865 if (item_selected)
1866 SetItemDefaultFocus();
1867 PopID();
1868 }
1869
1870 EndCombo();
1871
1872 if (value_changed)
1873 MarkItemEdited(id: g.LastItemData.ID);
1874
1875 return value_changed;
1876}
1877
1878// Combo box helper allowing to pass an array of strings.
1879bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
1880{
1881 const bool value_changed = Combo(label, current_item, items_getter: Items_ArrayGetter, data: (void*)items, items_count, popup_max_height_in_items: height_in_items);
1882 return value_changed;
1883}
1884
1885// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
1886bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
1887{
1888 int items_count = 0;
1889 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
1890 while (*p)
1891 {
1892 p += strlen(s: p) + 1;
1893 items_count++;
1894 }
1895 bool value_changed = Combo(label, current_item, items_getter: Items_SingleStringGetter, data: (void*)items_separated_by_zeros, items_count, popup_max_height_in_items: height_in_items);
1896 return value_changed;
1897}
1898
1899//-------------------------------------------------------------------------
1900// [SECTION] Data Type and Data Formatting Helpers [Internal]
1901//-------------------------------------------------------------------------
1902// - DataTypeGetInfo()
1903// - DataTypeFormatString()
1904// - DataTypeApplyOp()
1905// - DataTypeApplyOpFromText()
1906// - DataTypeCompare()
1907// - DataTypeClamp()
1908// - GetMinimumStepAtDecimalPrecision
1909// - RoundScalarWithFormat<>()
1910//-------------------------------------------------------------------------
1911
1912static const ImGuiDataTypeInfo GDataTypeInfo[] =
1913{
1914 { .Size: sizeof(char), .Name: "S8", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S8
1915 { .Size: sizeof(unsigned char), .Name: "U8", .PrintFmt: "%u", .ScanFmt: "%u" },
1916 { .Size: sizeof(short), .Name: "S16", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S16
1917 { .Size: sizeof(unsigned short), .Name: "U16", .PrintFmt: "%u", .ScanFmt: "%u" },
1918 { .Size: sizeof(int), .Name: "S32", .PrintFmt: "%d", .ScanFmt: "%d" }, // ImGuiDataType_S32
1919 { .Size: sizeof(unsigned int), .Name: "U32", .PrintFmt: "%u", .ScanFmt: "%u" },
1920#ifdef _MSC_VER
1921 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
1922 { sizeof(ImU64), "U64", "%I64u","%I64u" },
1923#else
1924 { .Size: sizeof(ImS64), .Name: "S64", .PrintFmt: "%lld", .ScanFmt: "%lld" }, // ImGuiDataType_S64
1925 { .Size: sizeof(ImU64), .Name: "U64", .PrintFmt: "%llu", .ScanFmt: "%llu" },
1926#endif
1927 { .Size: sizeof(float), .Name: "float", .PrintFmt: "%.3f",.ScanFmt: "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
1928 { .Size: sizeof(double), .Name: "double",.PrintFmt: "%f", .ScanFmt: "%lf" }, // ImGuiDataType_Double
1929};
1930IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
1931
1932const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
1933{
1934 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
1935 return &GDataTypeInfo[data_type];
1936}
1937
1938int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
1939{
1940 // Signedness doesn't matter when pushing integer arguments
1941 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
1942 return ImFormatString(buf, buf_size, fmt: format, *(const ImU32*)p_data);
1943 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
1944 return ImFormatString(buf, buf_size, fmt: format, *(const ImU64*)p_data);
1945 if (data_type == ImGuiDataType_Float)
1946 return ImFormatString(buf, buf_size, fmt: format, *(const float*)p_data);
1947 if (data_type == ImGuiDataType_Double)
1948 return ImFormatString(buf, buf_size, fmt: format, *(const double*)p_data);
1949 if (data_type == ImGuiDataType_S8)
1950 return ImFormatString(buf, buf_size, fmt: format, *(const ImS8*)p_data);
1951 if (data_type == ImGuiDataType_U8)
1952 return ImFormatString(buf, buf_size, fmt: format, *(const ImU8*)p_data);
1953 if (data_type == ImGuiDataType_S16)
1954 return ImFormatString(buf, buf_size, fmt: format, *(const ImS16*)p_data);
1955 if (data_type == ImGuiDataType_U16)
1956 return ImFormatString(buf, buf_size, fmt: format, *(const ImU16*)p_data);
1957 IM_ASSERT(0);
1958 return 0;
1959}
1960
1961void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
1962{
1963 IM_ASSERT(op == '+' || op == '-');
1964 switch (data_type)
1965 {
1966 case ImGuiDataType_S8:
1967 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
1968 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(a: *(const ImS8*)arg1, b: *(const ImS8*)arg2, mn: IM_S8_MIN, mx: IM_S8_MAX); }
1969 return;
1970 case ImGuiDataType_U8:
1971 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
1972 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(a: *(const ImU8*)arg1, b: *(const ImU8*)arg2, mn: IM_U8_MIN, mx: IM_U8_MAX); }
1973 return;
1974 case ImGuiDataType_S16:
1975 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
1976 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(a: *(const ImS16*)arg1, b: *(const ImS16*)arg2, mn: IM_S16_MIN, mx: IM_S16_MAX); }
1977 return;
1978 case ImGuiDataType_U16:
1979 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
1980 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(a: *(const ImU16*)arg1, b: *(const ImU16*)arg2, mn: IM_U16_MIN, mx: IM_U16_MAX); }
1981 return;
1982 case ImGuiDataType_S32:
1983 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
1984 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(a: *(const ImS32*)arg1, b: *(const ImS32*)arg2, mn: IM_S32_MIN, mx: IM_S32_MAX); }
1985 return;
1986 case ImGuiDataType_U32:
1987 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
1988 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(a: *(const ImU32*)arg1, b: *(const ImU32*)arg2, mn: IM_U32_MIN, mx: IM_U32_MAX); }
1989 return;
1990 case ImGuiDataType_S64:
1991 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
1992 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(a: *(const ImS64*)arg1, b: *(const ImS64*)arg2, mn: IM_S64_MIN, mx: IM_S64_MAX); }
1993 return;
1994 case ImGuiDataType_U64:
1995 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
1996 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(a: *(const ImU64*)arg1, b: *(const ImU64*)arg2, mn: IM_U64_MIN, mx: IM_U64_MAX); }
1997 return;
1998 case ImGuiDataType_Float:
1999 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2000 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2001 return;
2002 case ImGuiDataType_Double:
2003 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2004 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2005 return;
2006 case ImGuiDataType_COUNT: break;
2007 }
2008 IM_ASSERT(0);
2009}
2010
2011// User can input math operators (e.g. +100) to edit a numerical values.
2012// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2013bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format)
2014{
2015 while (ImCharIsBlankA(c: *buf))
2016 buf++;
2017 if (!buf[0])
2018 return false;
2019
2020 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2021 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2022 ImGuiDataTypeTempStorage data_backup;
2023 memcpy(dest: &data_backup, src: p_data, n: type_info->Size);
2024
2025 // Sanitize format
2026 // 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
2027 char format_sanitized[32];
2028 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2029 format = type_info->ScanFmt;
2030 else
2031 format = ImParseFormatSanitizeForScanning(fmt_in: format, fmt_out: format_sanitized, IM_ARRAYSIZE(format_sanitized));
2032
2033 // Small types need a 32-bit buffer to receive the result from scanf()
2034 int v32 = 0;
2035 if (sscanf(s: buf, format: format, type_info->Size >= 4 ? p_data : &v32) < 1)
2036 return false;
2037 if (type_info->Size < 4)
2038 {
2039 if (data_type == ImGuiDataType_S8)
2040 *(ImS8*)p_data = (ImS8)ImClamp(v: v32, mn: (int)IM_S8_MIN, mx: (int)IM_S8_MAX);
2041 else if (data_type == ImGuiDataType_U8)
2042 *(ImU8*)p_data = (ImU8)ImClamp(v: v32, mn: (int)IM_U8_MIN, mx: (int)IM_U8_MAX);
2043 else if (data_type == ImGuiDataType_S16)
2044 *(ImS16*)p_data = (ImS16)ImClamp(v: v32, mn: (int)IM_S16_MIN, mx: (int)IM_S16_MAX);
2045 else if (data_type == ImGuiDataType_U16)
2046 *(ImU16*)p_data = (ImU16)ImClamp(v: v32, mn: (int)IM_U16_MIN, mx: (int)IM_U16_MAX);
2047 else
2048 IM_ASSERT(0);
2049 }
2050
2051 return memcmp(s1: &data_backup, s2: p_data, n: type_info->Size) != 0;
2052}
2053
2054template<typename T>
2055static int DataTypeCompareT(const T* lhs, const T* rhs)
2056{
2057 if (*lhs < *rhs) return -1;
2058 if (*lhs > *rhs) return +1;
2059 return 0;
2060}
2061
2062int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2063{
2064 switch (data_type)
2065 {
2066 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >(lhs: (const ImS8* )arg_1, rhs: (const ImS8* )arg_2);
2067 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >(lhs: (const ImU8* )arg_1, rhs: (const ImU8* )arg_2);
2068 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >(lhs: (const ImS16* )arg_1, rhs: (const ImS16* )arg_2);
2069 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >(lhs: (const ImU16* )arg_1, rhs: (const ImU16* )arg_2);
2070 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >(lhs: (const ImS32* )arg_1, rhs: (const ImS32* )arg_2);
2071 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >(lhs: (const ImU32* )arg_1, rhs: (const ImU32* )arg_2);
2072 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >(lhs: (const ImS64* )arg_1, rhs: (const ImS64* )arg_2);
2073 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >(lhs: (const ImU64* )arg_1, rhs: (const ImU64* )arg_2);
2074 case ImGuiDataType_Float: return DataTypeCompareT<float >(lhs: (const float* )arg_1, rhs: (const float* )arg_2);
2075 case ImGuiDataType_Double: return DataTypeCompareT<double>(lhs: (const double*)arg_1, rhs: (const double*)arg_2);
2076 case ImGuiDataType_COUNT: break;
2077 }
2078 IM_ASSERT(0);
2079 return 0;
2080}
2081
2082template<typename T>
2083static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2084{
2085 // Clamp, both sides are optional, return true if modified
2086 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2087 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2088 return false;
2089}
2090
2091bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2092{
2093 switch (data_type)
2094 {
2095 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >(v: (ImS8* )p_data, v_min: (const ImS8* )p_min, v_max: (const ImS8* )p_max);
2096 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >(v: (ImU8* )p_data, v_min: (const ImU8* )p_min, v_max: (const ImU8* )p_max);
2097 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >(v: (ImS16* )p_data, v_min: (const ImS16* )p_min, v_max: (const ImS16* )p_max);
2098 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >(v: (ImU16* )p_data, v_min: (const ImU16* )p_min, v_max: (const ImU16* )p_max);
2099 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >(v: (ImS32* )p_data, v_min: (const ImS32* )p_min, v_max: (const ImS32* )p_max);
2100 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >(v: (ImU32* )p_data, v_min: (const ImU32* )p_min, v_max: (const ImU32* )p_max);
2101 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >(v: (ImS64* )p_data, v_min: (const ImS64* )p_min, v_max: (const ImS64* )p_max);
2102 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >(v: (ImU64* )p_data, v_min: (const ImU64* )p_min, v_max: (const ImU64* )p_max);
2103 case ImGuiDataType_Float: return DataTypeClampT<float >(v: (float* )p_data, v_min: (const float* )p_min, v_max: (const float* )p_max);
2104 case ImGuiDataType_Double: return DataTypeClampT<double>(v: (double*)p_data, v_min: (const double*)p_min, v_max: (const double*)p_max);
2105 case ImGuiDataType_COUNT: break;
2106 }
2107 IM_ASSERT(0);
2108 return false;
2109}
2110
2111static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2112{
2113 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 };
2114 if (decimal_precision < 0)
2115 return FLT_MIN;
2116 return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(x: 10.0f, y: (float)-decimal_precision);
2117}
2118
2119template<typename TYPE>
2120TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2121{
2122 IM_UNUSED(data_type);
2123 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2124 const char* fmt_start = ImParseFormatFindStart(format);
2125 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2126 return v;
2127
2128 // Sanitize format
2129 char fmt_sanitized[32];
2130 ImParseFormatSanitizeForPrinting(fmt_in: fmt_start, fmt_out: fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2131 fmt_start = fmt_sanitized;
2132
2133 // Format value with our rounding, and read back
2134 char v_str[64];
2135 ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2136 const char* p = v_str;
2137 while (*p == ' ')
2138 p++;
2139 v = (TYPE)ImAtof(p);
2140
2141 return v;
2142}
2143
2144//-------------------------------------------------------------------------
2145// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2146//-------------------------------------------------------------------------
2147// - DragBehaviorT<>() [Internal]
2148// - DragBehavior() [Internal]
2149// - DragScalar()
2150// - DragScalarN()
2151// - DragFloat()
2152// - DragFloat2()
2153// - DragFloat3()
2154// - DragFloat4()
2155// - DragFloatRange2()
2156// - DragInt()
2157// - DragInt2()
2158// - DragInt3()
2159// - DragInt4()
2160// - DragIntRange2()
2161//-------------------------------------------------------------------------
2162
2163// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2164template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2165bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2166{
2167 ImGuiContext& g = *GImGui;
2168 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2169 const bool is_clamped = (v_min < v_max);
2170 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2171 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2172
2173 // Default tweak speed
2174 if (v_speed == 0.0f && is_clamped && (v_max - v_min < FLT_MAX))
2175 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2176
2177 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2178 float adjust_delta = 0.0f;
2179 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2180 {
2181 adjust_delta = g.IO.MouseDelta[axis];
2182 if (g.IO.KeyAlt)
2183 adjust_delta *= 1.0f / 100.0f;
2184 if (g.IO.KeyShift)
2185 adjust_delta *= 10.0f;
2186 }
2187 else if (g.ActiveIdSource == ImGuiInputSource_Nav)
2188 {
2189 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2190 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2191 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2192 const float tweak_factor = tweak_slow ? 1.0f / 1.0f : tweak_fast ? 10.0f : 1.0f;
2193 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2194 v_speed = ImMax(lhs: v_speed, rhs: GetMinimumStepAtDecimalPrecision(decimal_precision));
2195 }
2196 adjust_delta *= v_speed;
2197
2198 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2199 if (axis == ImGuiAxis_Y)
2200 adjust_delta = -adjust_delta;
2201
2202 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2203 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2204 adjust_delta /= (float)(v_max - v_min);
2205
2206 // Clear current value on activation
2207 // 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.
2208 bool is_just_activated = g.ActiveIdIsJustActivated;
2209 bool is_already_past_limits_and_pushing_outward = is_clamped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f));
2210 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2211 {
2212 g.DragCurrentAccum = 0.0f;
2213 g.DragCurrentAccumDirty = false;
2214 }
2215 else if (adjust_delta != 0.0f)
2216 {
2217 g.DragCurrentAccum += adjust_delta;
2218 g.DragCurrentAccumDirty = true;
2219 }
2220
2221 if (!g.DragCurrentAccumDirty)
2222 return false;
2223
2224 TYPE v_cur = *v;
2225 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2226
2227 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2228 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2229 if (is_logarithmic)
2230 {
2231 // 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.
2232 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2233 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2234
2235 // Convert to parametric space, apply delta, convert back
2236 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2237 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2238 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2239 v_old_ref_for_accum_remainder = v_old_parametric;
2240 }
2241 else
2242 {
2243 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2244 }
2245
2246 // Round to user desired precision based on format string
2247 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2248 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2249
2250 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2251 g.DragCurrentAccumDirty = false;
2252 if (is_logarithmic)
2253 {
2254 // Convert to parametric space, apply delta, convert back
2255 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2256 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2257 }
2258 else
2259 {
2260 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2261 }
2262
2263 // Lose zero sign for float/double
2264 if (v_cur == (TYPE)-0)
2265 v_cur = (TYPE)0;
2266
2267 // Clamp values (+ handle overflow/wrap-around for integer types)
2268 if (*v != v_cur && is_clamped)
2269 {
2270 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2271 v_cur = v_min;
2272 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2273 v_cur = v_max;
2274 }
2275
2276 // Apply result
2277 if (*v == v_cur)
2278 return false;
2279 *v = v_cur;
2280 return true;
2281}
2282
2283bool 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)
2284{
2285 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2286 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.");
2287
2288 ImGuiContext& g = *GImGui;
2289 if (g.ActiveId == id)
2290 {
2291 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2292 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2293 ClearActiveID();
2294 else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2295 ClearActiveID();
2296 }
2297 if (g.ActiveId != id)
2298 return false;
2299 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2300 return false;
2301
2302 switch (data_type)
2303 {
2304 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; }
2305 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; }
2306 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; }
2307 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; }
2308 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);
2309 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);
2310 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);
2311 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);
2312 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);
2313 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);
2314 case ImGuiDataType_COUNT: break;
2315 }
2316 IM_ASSERT(0);
2317 return false;
2318}
2319
2320// 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.
2321// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2322bool 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)
2323{
2324 ImGuiWindow* window = GetCurrentWindow();
2325 if (window->SkipItems)
2326 return false;
2327
2328 ImGuiContext& g = *GImGui;
2329 const ImGuiStyle& style = g.Style;
2330 const ImGuiID id = window->GetID(str: label);
2331 const float w = CalcItemWidth();
2332
2333 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2334 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2335 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));
2336
2337 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2338 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2339 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2340 return false;
2341
2342 // Default format string when passing NULL
2343 if (format == NULL)
2344 format = DataTypeGetInfo(data_type)->PrintFmt;
2345
2346 const bool hovered = ItemHoverable(bb: frame_bb, id);
2347 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2348 if (!temp_input_is_active)
2349 {
2350 // Tabbing or CTRL-clicking on Drag turns it into an InputText
2351 const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
2352 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
2353 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id));
2354 const bool make_active = (input_requested_by_tabbing || clicked || double_clicked || g.NavActivateId == id || g.NavActivateInputId == id);
2355 if (make_active && (clicked || double_clicked))
2356 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2357 if (make_active && temp_input_allowed)
2358 if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || double_clicked || g.NavActivateInputId == id)
2359 temp_input_is_active = true;
2360
2361 // (Optional) simple click (without moving) turns Drag into an InputText
2362 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2363 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(button: 0, lock_threshold: g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2364 {
2365 g.NavActivateId = g.NavActivateInputId = id;
2366 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2367 temp_input_is_active = true;
2368 }
2369
2370 if (make_active && !temp_input_is_active)
2371 {
2372 SetActiveID(id, window);
2373 SetFocusID(id, window);
2374 FocusWindow(window);
2375 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2376 }
2377 }
2378
2379 if (temp_input_is_active)
2380 {
2381 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
2382 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);
2383 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);
2384 }
2385
2386 // Draw frame
2387 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2388 RenderNavHighlight(bb: frame_bb, id);
2389 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: style.FrameRounding);
2390
2391 // Drag behavior
2392 const bool value_changed = DragBehavior(id, data_type, p_v: p_data, v_speed, p_min, p_max, format, flags);
2393 if (value_changed)
2394 MarkItemEdited(id);
2395
2396 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2397 char value_buf[64];
2398 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2399 if (g.LogEnabled)
2400 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2401 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));
2402
2403 if (label_size.x > 0.0f)
2404 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2405
2406 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
2407 return value_changed;
2408}
2409
2410bool 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)
2411{
2412 ImGuiWindow* window = GetCurrentWindow();
2413 if (window->SkipItems)
2414 return false;
2415
2416 ImGuiContext& g = *GImGui;
2417 bool value_changed = false;
2418 BeginGroup();
2419 PushID(str_id: label);
2420 PushMultiItemsWidths(components, width_full: CalcItemWidth());
2421 size_t type_size = GDataTypeInfo[data_type].Size;
2422 for (int i = 0; i < components; i++)
2423 {
2424 PushID(int_id: i);
2425 if (i > 0)
2426 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2427 value_changed |= DragScalar(label: "", data_type, p_data, v_speed, p_min, p_max, format, flags);
2428 PopID();
2429 PopItemWidth();
2430 p_data = (void*)((char*)p_data + type_size);
2431 }
2432 PopID();
2433
2434 const char* label_end = FindRenderedTextEnd(text: label);
2435 if (label != label_end)
2436 {
2437 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2438 TextEx(text: label, text_end: label_end);
2439 }
2440
2441 EndGroup();
2442 return value_changed;
2443}
2444
2445bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2446{
2447 return DragScalar(label, data_type: ImGuiDataType_Float, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2448}
2449
2450bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2451{
2452 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2453}
2454
2455bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2456{
2457 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2458}
2459
2460bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2461{
2462 return DragScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2463}
2464
2465// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2466bool 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)
2467{
2468 ImGuiWindow* window = GetCurrentWindow();
2469 if (window->SkipItems)
2470 return false;
2471
2472 ImGuiContext& g = *GImGui;
2473 PushID(str_id: label);
2474 BeginGroup();
2475 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2476
2477 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2478 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2479 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2480 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);
2481 PopItemWidth();
2482 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2483
2484 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2485 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2486 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2487 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);
2488 PopItemWidth();
2489 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2490
2491 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2492 EndGroup();
2493 PopID();
2494
2495 return value_changed;
2496}
2497
2498// NB: v_speed is float to allow adjusting the drag speed with more precision
2499bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2500{
2501 return DragScalar(label, data_type: ImGuiDataType_S32, p_data: v, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2502}
2503
2504bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2505{
2506 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2507}
2508
2509bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2510{
2511 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2512}
2513
2514bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2515{
2516 return DragScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, v_speed, p_min: &v_min, p_max: &v_max, format, flags);
2517}
2518
2519// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2520bool 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)
2521{
2522 ImGuiWindow* window = GetCurrentWindow();
2523 if (window->SkipItems)
2524 return false;
2525
2526 ImGuiContext& g = *GImGui;
2527 PushID(str_id: label);
2528 BeginGroup();
2529 PushMultiItemsWidths(components: 2, width_full: CalcItemWidth());
2530
2531 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2532 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(lhs: v_max, rhs: *v_current_max);
2533 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2534 bool value_changed = DragInt(label: "##min", v: v_current_min, v_speed, v_min: min_min, v_max: min_max, format, flags: min_flags);
2535 PopItemWidth();
2536 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2537
2538 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(lhs: v_min, rhs: *v_current_min);
2539 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2540 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2541 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);
2542 PopItemWidth();
2543 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
2544
2545 TextEx(text: label, text_end: FindRenderedTextEnd(text: label));
2546 EndGroup();
2547 PopID();
2548
2549 return value_changed;
2550}
2551
2552//-------------------------------------------------------------------------
2553// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2554//-------------------------------------------------------------------------
2555// - ScaleRatioFromValueT<> [Internal]
2556// - ScaleValueFromRatioT<> [Internal]
2557// - SliderBehaviorT<>() [Internal]
2558// - SliderBehavior() [Internal]
2559// - SliderScalar()
2560// - SliderScalarN()
2561// - SliderFloat()
2562// - SliderFloat2()
2563// - SliderFloat3()
2564// - SliderFloat4()
2565// - SliderAngle()
2566// - SliderInt()
2567// - SliderInt2()
2568// - SliderInt3()
2569// - SliderInt4()
2570// - VSliderScalar()
2571// - VSliderFloat()
2572// - VSliderInt()
2573//-------------------------------------------------------------------------
2574
2575// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2576template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2577float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2578{
2579 if (v_min == v_max)
2580 return 0.0f;
2581 IM_UNUSED(data_type);
2582
2583 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2584 if (is_logarithmic)
2585 {
2586 bool flipped = v_max < v_min;
2587
2588 if (flipped) // Handle the case where the range is backwards
2589 ImSwap(v_min, v_max);
2590
2591 // Fudge min/max to avoid getting close to log(0)
2592 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2593 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2594
2595 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2596 if ((v_min == 0.0f) && (v_max < 0.0f))
2597 v_min_fudged = -logarithmic_zero_epsilon;
2598 else if ((v_max == 0.0f) && (v_min < 0.0f))
2599 v_max_fudged = -logarithmic_zero_epsilon;
2600
2601 float result;
2602 if (v_clamped <= v_min_fudged)
2603 result = 0.0f; // Workaround for values that are in-range but below our fudge
2604 else if (v_clamped >= v_max_fudged)
2605 result = 1.0f; // Workaround for values that are in-range but above our fudge
2606 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2607 {
2608 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)
2609 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2610 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2611 if (v == 0.0f)
2612 result = zero_point_center; // Special case for exactly zero
2613 else if (v < 0.0f)
2614 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2615 else
2616 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));
2617 }
2618 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2619 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2620 else
2621 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2622
2623 return flipped ? (1.0f - result) : result;
2624 }
2625 else
2626 {
2627 // Linear slider
2628 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2629 }
2630}
2631
2632// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2633template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2634TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2635{
2636 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2637 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2638 if (t <= 0.0f || v_min == v_max)
2639 return v_min;
2640 if (t >= 1.0f)
2641 return v_max;
2642
2643 TYPE result = (TYPE)0;
2644 if (is_logarithmic)
2645 {
2646 // Fudge min/max to avoid getting silly results close to zero
2647 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2648 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2649
2650 const bool flipped = v_max < v_min; // Check if range is "backwards"
2651 if (flipped)
2652 ImSwap(v_min_fudged, v_max_fudged);
2653
2654 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2655 if ((v_max == 0.0f) && (v_min < 0.0f))
2656 v_max_fudged = -logarithmic_zero_epsilon;
2657
2658 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2659
2660 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2661 {
2662 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs(x: (float)v_max - (float)v_min); // The zero point in parametric space
2663 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2664 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2665 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2666 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2667 else if (t_with_flip < zero_point_center)
2668 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2669 else
2670 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))));
2671 }
2672 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2673 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2674 else
2675 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2676 }
2677 else
2678 {
2679 // Linear slider
2680 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2681 if (is_floating_point)
2682 {
2683 result = ImLerp(v_min, v_max, t);
2684 }
2685 else if (t < 1.0)
2686 {
2687 // - For integer values we want the clicking position to match the grab box so we round above
2688 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
2689 // - 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
2690 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
2691 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
2692 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
2693 }
2694 }
2695
2696 return result;
2697}
2698
2699// FIXME: Try to move more of the code into shared SliderBehavior()
2700template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2701bool 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)
2702{
2703 ImGuiContext& g = *GImGui;
2704 const ImGuiStyle& style = g.Style;
2705
2706 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2707 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2708 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2709 const SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max);
2710
2711 // Calculate bounds
2712 const float grab_padding = 2.0f; // FIXME: Should be part of style.
2713 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
2714 float grab_sz = style.GrabMinSize;
2715 if (!is_floating_point && v_range >= 0) // v_range < 0 may happen on integer overflows
2716 grab_sz = ImMax(lhs: (float)(slider_sz / (v_range + 1)), rhs: style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
2717 grab_sz = ImMin(lhs: grab_sz, rhs: slider_sz);
2718 const float slider_usable_sz = slider_sz - grab_sz;
2719 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
2720 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
2721
2722 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2723 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
2724 if (is_logarithmic)
2725 {
2726 // 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.
2727 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 1;
2728 logarithmic_zero_epsilon = ImPow(x: 0.1f, y: (float)decimal_precision);
2729 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(lhs: slider_usable_sz, rhs: 1.0f);
2730 }
2731
2732 // Process interacting with the slider
2733 bool value_changed = false;
2734 if (g.ActiveId == id)
2735 {
2736 bool set_new_value = false;
2737 float clicked_t = 0.0f;
2738 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
2739 {
2740 if (!g.IO.MouseDown[0])
2741 {
2742 ClearActiveID();
2743 }
2744 else
2745 {
2746 const float mouse_abs_pos = g.IO.MousePos[axis];
2747 if (g.ActiveIdIsJustActivated)
2748 {
2749 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2750 if (axis == ImGuiAxis_Y)
2751 grab_t = 1.0f - grab_t;
2752 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
2753 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.
2754 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
2755 }
2756 if (slider_usable_sz > 0.0f)
2757 clicked_t = ImSaturate(f: (mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
2758 if (axis == ImGuiAxis_Y)
2759 clicked_t = 1.0f - clicked_t;
2760 set_new_value = true;
2761 }
2762 }
2763 else if (g.ActiveIdSource == ImGuiInputSource_Nav)
2764 {
2765 if (g.ActiveIdIsJustActivated)
2766 {
2767 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
2768 g.SliderCurrentAccumDirty = false;
2769 }
2770
2771 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
2772 if (input_delta != 0.0f)
2773 {
2774 const bool tweak_slow = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2775 const bool tweak_fast = IsKeyDown(key: (g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2776 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, default_value: 3) : 0;
2777 if (decimal_precision > 0)
2778 {
2779 input_delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds
2780 if (tweak_slow)
2781 input_delta /= 10.0f;
2782 }
2783 else
2784 {
2785 if ((v_range >= -100.0f && v_range <= 100.0f) || tweak_slow)
2786 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps
2787 else
2788 input_delta /= 100.0f;
2789 }
2790 if (tweak_fast)
2791 input_delta *= 10.0f;
2792
2793 g.SliderCurrentAccum += input_delta;
2794 g.SliderCurrentAccumDirty = true;
2795 }
2796
2797 float delta = g.SliderCurrentAccum;
2798 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2799 {
2800 ClearActiveID();
2801 }
2802 else if (g.SliderCurrentAccumDirty)
2803 {
2804 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2805
2806 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
2807 {
2808 set_new_value = false;
2809 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
2810 }
2811 else
2812 {
2813 set_new_value = true;
2814 float old_clicked_t = clicked_t;
2815 clicked_t = ImSaturate(f: clicked_t + delta);
2816
2817 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
2818 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2819 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2820 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
2821 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2822
2823 if (delta > 0)
2824 g.SliderCurrentAccum -= ImMin(lhs: new_clicked_t - old_clicked_t, rhs: delta);
2825 else
2826 g.SliderCurrentAccum -= ImMax(lhs: new_clicked_t - old_clicked_t, rhs: delta);
2827 }
2828
2829 g.SliderCurrentAccumDirty = false;
2830 }
2831 }
2832
2833 if (set_new_value)
2834 {
2835 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2836
2837 // Round to user desired precision based on format string
2838 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2839 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
2840
2841 // Apply result
2842 if (*v != v_new)
2843 {
2844 *v = v_new;
2845 value_changed = true;
2846 }
2847 }
2848 }
2849
2850 if (slider_sz < 1.0f)
2851 {
2852 *out_grab_bb = ImRect(bb.Min, bb.Min);
2853 }
2854 else
2855 {
2856 // Output grab position so it can be displayed by the caller
2857 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2858 if (axis == ImGuiAxis_Y)
2859 grab_t = 1.0f - grab_t;
2860 const float grab_pos = ImLerp(a: slider_usable_pos_min, b: slider_usable_pos_max, t: grab_t);
2861 if (axis == ImGuiAxis_X)
2862 *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);
2863 else
2864 *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);
2865 }
2866
2867 return value_changed;
2868}
2869
2870// For 32-bit and larger types, slider bounds are limited to half the natural type range.
2871// 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.
2872// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
2873bool 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)
2874{
2875 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2876 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.");
2877
2878 // Those are the things we can do easily outside the SliderBehaviorT<> template, saves code generation.
2879 ImGuiContext& g = *GImGui;
2880 if ((g.LastItemData.InFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2881 return false;
2882
2883 switch (data_type)
2884 {
2885 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; }
2886 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; }
2887 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; }
2888 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; }
2889 case ImGuiDataType_S32:
2890 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
2891 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);
2892 case ImGuiDataType_U32:
2893 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
2894 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);
2895 case ImGuiDataType_S64:
2896 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
2897 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);
2898 case ImGuiDataType_U64:
2899 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
2900 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);
2901 case ImGuiDataType_Float:
2902 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
2903 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);
2904 case ImGuiDataType_Double:
2905 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
2906 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);
2907 case ImGuiDataType_COUNT: break;
2908 }
2909 IM_ASSERT(0);
2910 return false;
2911}
2912
2913// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
2914// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2915bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
2916{
2917 ImGuiWindow* window = GetCurrentWindow();
2918 if (window->SkipItems)
2919 return false;
2920
2921 ImGuiContext& g = *GImGui;
2922 const ImGuiStyle& style = g.Style;
2923 const ImGuiID id = window->GetID(str: label);
2924 const float w = CalcItemWidth();
2925
2926 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
2927 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2928 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));
2929
2930 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2931 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
2932 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2933 return false;
2934
2935 // Default format string when passing NULL
2936 if (format == NULL)
2937 format = DataTypeGetInfo(data_type)->PrintFmt;
2938
2939 const bool hovered = ItemHoverable(bb: frame_bb, id);
2940 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2941 if (!temp_input_is_active)
2942 {
2943 // Tabbing or CTRL-clicking on Slider turns it into an input box
2944 const bool input_requested_by_tabbing = temp_input_allowed && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
2945 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
2946 const bool make_active = (input_requested_by_tabbing || clicked || g.NavActivateId == id || g.NavActivateInputId == id);
2947 if (make_active && clicked)
2948 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
2949 if (make_active && temp_input_allowed)
2950 if (input_requested_by_tabbing || (clicked && g.IO.KeyCtrl) || g.NavActivateInputId == id)
2951 temp_input_is_active = true;
2952
2953 if (make_active && !temp_input_is_active)
2954 {
2955 SetActiveID(id, window);
2956 SetFocusID(id, window);
2957 FocusWindow(window);
2958 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2959 }
2960 }
2961
2962 if (temp_input_is_active)
2963 {
2964 // Only clamp CTRL+Click input when ImGuiSliderFlags_AlwaysClamp is set
2965 const bool is_clamp_input = (flags & ImGuiSliderFlags_AlwaysClamp) != 0;
2966 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);
2967 }
2968
2969 // Draw frame
2970 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2971 RenderNavHighlight(bb: frame_bb, id);
2972 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
2973
2974 // Slider behavior
2975 ImRect grab_bb;
2976 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);
2977 if (value_changed)
2978 MarkItemEdited(id);
2979
2980 // Render grab
2981 if (grab_bb.Max.x > grab_bb.Min.x)
2982 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);
2983
2984 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2985 char value_buf[64];
2986 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2987 if (g.LogEnabled)
2988 LogSetNextTextDecoration(prefix: "{", suffix: "}");
2989 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));
2990
2991 if (label_size.x > 0.0f)
2992 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
2993
2994 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
2995 return value_changed;
2996}
2997
2998// Add multiple sliders on 1 line for compact edition of multiple components
2999bool 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)
3000{
3001 ImGuiWindow* window = GetCurrentWindow();
3002 if (window->SkipItems)
3003 return false;
3004
3005 ImGuiContext& g = *GImGui;
3006 bool value_changed = false;
3007 BeginGroup();
3008 PushID(str_id: label);
3009 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3010 size_t type_size = GDataTypeInfo[data_type].Size;
3011 for (int i = 0; i < components; i++)
3012 {
3013 PushID(int_id: i);
3014 if (i > 0)
3015 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3016 value_changed |= SliderScalar(label: "", data_type, p_data: v, p_min: v_min, p_max: v_max, format, flags);
3017 PopID();
3018 PopItemWidth();
3019 v = (void*)((char*)v + type_size);
3020 }
3021 PopID();
3022
3023 const char* label_end = FindRenderedTextEnd(text: label);
3024 if (label != label_end)
3025 {
3026 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3027 TextEx(text: label, text_end: label_end);
3028 }
3029
3030 EndGroup();
3031 return value_changed;
3032}
3033
3034bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3035{
3036 return SliderScalar(label, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3037}
3038
3039bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3040{
3041 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3042}
3043
3044bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3045{
3046 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3047}
3048
3049bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3050{
3051 return SliderScalarN(label, data_type: ImGuiDataType_Float, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3052}
3053
3054bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3055{
3056 if (format == NULL)
3057 format = "%.0f deg";
3058 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3059 bool value_changed = SliderFloat(label, v: &v_deg, v_min: v_degrees_min, v_max: v_degrees_max, format, flags);
3060 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3061 return value_changed;
3062}
3063
3064bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3065{
3066 return SliderScalar(label, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3067}
3068
3069bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3070{
3071 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 2, v_min: &v_min, v_max: &v_max, format, flags);
3072}
3073
3074bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3075{
3076 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 3, v_min: &v_min, v_max: &v_max, format, flags);
3077}
3078
3079bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3080{
3081 return SliderScalarN(label, data_type: ImGuiDataType_S32, v, components: 4, v_min: &v_min, v_max: &v_max, format, flags);
3082}
3083
3084bool 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)
3085{
3086 ImGuiWindow* window = GetCurrentWindow();
3087 if (window->SkipItems)
3088 return false;
3089
3090 ImGuiContext& g = *GImGui;
3091 const ImGuiStyle& style = g.Style;
3092 const ImGuiID id = window->GetID(str: label);
3093
3094 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3095 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3096 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));
3097
3098 ItemSize(bb, text_baseline_y: style.FramePadding.y);
3099 if (!ItemAdd(bb: frame_bb, id))
3100 return false;
3101
3102 // Default format string when passing NULL
3103 if (format == NULL)
3104 format = DataTypeGetInfo(data_type)->PrintFmt;
3105
3106 const bool hovered = ItemHoverable(bb: frame_bb, id);
3107 const bool clicked = hovered && IsMouseClicked(button: 0, owner_id: id);
3108 if (clicked || g.NavActivateId == id || g.NavActivateInputId == id)
3109 {
3110 if (clicked)
3111 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
3112 SetActiveID(id, window);
3113 SetFocusID(id, window);
3114 FocusWindow(window);
3115 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3116 }
3117
3118 // Draw frame
3119 const ImU32 frame_col = GetColorU32(idx: g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3120 RenderNavHighlight(bb: frame_bb, id);
3121 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: frame_col, border: true, rounding: g.Style.FrameRounding);
3122
3123 // Slider behavior
3124 ImRect grab_bb;
3125 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);
3126 if (value_changed)
3127 MarkItemEdited(id);
3128
3129 // Render grab
3130 if (grab_bb.Max.y > grab_bb.Min.y)
3131 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);
3132
3133 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3134 // For the vertical slider we allow centered text to overlap the frame padding
3135 char value_buf[64];
3136 const char* value_buf_end = value_buf + DataTypeFormatString(buf: value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3137 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));
3138 if (label_size.x > 0.0f)
3139 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
3140
3141 return value_changed;
3142}
3143
3144bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3145{
3146 return VSliderScalar(label, size, data_type: ImGuiDataType_Float, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3147}
3148
3149bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3150{
3151 return VSliderScalar(label, size, data_type: ImGuiDataType_S32, p_data: v, p_min: &v_min, p_max: &v_max, format, flags);
3152}
3153
3154//-------------------------------------------------------------------------
3155// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3156//-------------------------------------------------------------------------
3157// - ImParseFormatFindStart() [Internal]
3158// - ImParseFormatFindEnd() [Internal]
3159// - ImParseFormatTrimDecorations() [Internal]
3160// - ImParseFormatSanitizeForPrinting() [Internal]
3161// - ImParseFormatSanitizeForScanning() [Internal]
3162// - ImParseFormatPrecision() [Internal]
3163// - TempInputTextScalar() [Internal]
3164// - InputScalar()
3165// - InputScalarN()
3166// - InputFloat()
3167// - InputFloat2()
3168// - InputFloat3()
3169// - InputFloat4()
3170// - InputInt()
3171// - InputInt2()
3172// - InputInt3()
3173// - InputInt4()
3174// - InputDouble()
3175//-------------------------------------------------------------------------
3176
3177// We don't use strchr() because our strings are usually very short and often start with '%'
3178const char* ImParseFormatFindStart(const char* fmt)
3179{
3180 while (char c = fmt[0])
3181 {
3182 if (c == '%' && fmt[1] != '%')
3183 return fmt;
3184 else if (c == '%')
3185 fmt++;
3186 fmt++;
3187 }
3188 return fmt;
3189}
3190
3191const char* ImParseFormatFindEnd(const char* fmt)
3192{
3193 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3194 if (fmt[0] != '%')
3195 return fmt;
3196 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3197 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3198 for (char c; (c = *fmt) != 0; fmt++)
3199 {
3200 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3201 return fmt + 1;
3202 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3203 return fmt + 1;
3204 }
3205 return fmt;
3206}
3207
3208// Extract the format out of a format string with leading or trailing decorations
3209// fmt = "blah blah" -> return fmt
3210// fmt = "%.3f" -> return fmt
3211// fmt = "hello %.3f" -> return fmt + 6
3212// fmt = "%.3f hello" -> return buf written with "%.3f"
3213const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3214{
3215 const char* fmt_start = ImParseFormatFindStart(fmt);
3216 if (fmt_start[0] != '%')
3217 return fmt;
3218 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_start);
3219 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3220 return fmt_start;
3221 ImStrncpy(dst: buf, src: fmt_start, count: ImMin(lhs: (size_t)(fmt_end - fmt_start) + 1, rhs: buf_size));
3222 return buf;
3223}
3224
3225// Sanitize format
3226// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3227// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3228void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3229{
3230 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3231 IM_UNUSED(fmt_out_size);
3232 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3233 while (fmt_in < fmt_end)
3234 {
3235 char c = *fmt_in++;
3236 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3237 *(fmt_out++) = c;
3238 }
3239 *fmt_out = 0; // Zero-terminate
3240}
3241
3242// - For scanning we need to remove all width and precision fields "%3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d"
3243const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3244{
3245 const char* fmt_end = ImParseFormatFindEnd(fmt: fmt_in);
3246 const char* fmt_out_begin = fmt_out;
3247 IM_UNUSED(fmt_out_size);
3248 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3249 bool has_type = false;
3250 while (fmt_in < fmt_end)
3251 {
3252 char c = *fmt_in++;
3253 if (!has_type && ((c >= '0' && c <= '9') || c == '.'))
3254 continue;
3255 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3256 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3257 *(fmt_out++) = c;
3258 }
3259 *fmt_out = 0; // Zero-terminate
3260 return fmt_out_begin;
3261}
3262
3263template<typename TYPE>
3264static const char* ImAtoi(const char* src, TYPE* output)
3265{
3266 int negative = 0;
3267 if (*src == '-') { negative = 1; src++; }
3268 if (*src == '+') { src++; }
3269 TYPE v = 0;
3270 while (*src >= '0' && *src <= '9')
3271 v = (v * 10) + (*src++ - '0');
3272 *output = negative ? -v : v;
3273 return src;
3274}
3275
3276// Parse display precision back from the display format string
3277// 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.
3278int ImParseFormatPrecision(const char* fmt, int default_precision)
3279{
3280 fmt = ImParseFormatFindStart(fmt);
3281 if (fmt[0] != '%')
3282 return default_precision;
3283 fmt++;
3284 while (*fmt >= '0' && *fmt <= '9')
3285 fmt++;
3286 int precision = INT_MAX;
3287 if (*fmt == '.')
3288 {
3289 fmt = ImAtoi<int>(src: fmt + 1, output: &precision);
3290 if (precision < 0 || precision > 99)
3291 precision = default_precision;
3292 }
3293 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3294 precision = -1;
3295 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3296 precision = -1;
3297 return (precision == INT_MAX) ? default_precision : precision;
3298}
3299
3300// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3301// FIXME: Facilitate using this in variety of other situations.
3302bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3303{
3304 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3305 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3306 ImGuiContext& g = *GImGui;
3307 const bool init = (g.TempInputId != id);
3308 if (init)
3309 ClearActiveID();
3310
3311 g.CurrentWindow->DC.CursorPos = bb.Min;
3312 bool value_changed = InputTextEx(label, NULL, buf, buf_size, size_arg: bb.GetSize(), flags: flags | ImGuiInputTextFlags_MergedItem);
3313 if (init)
3314 {
3315 // First frame we started displaying the InputText widget, we expect it to take the active id.
3316 IM_ASSERT(g.ActiveId == id);
3317 g.TempInputId = g.ActiveId;
3318 }
3319 return value_changed;
3320}
3321
3322static inline ImGuiInputTextFlags InputScalar_DefaultCharsFilter(ImGuiDataType data_type, const char* format)
3323{
3324 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
3325 return ImGuiInputTextFlags_CharsScientific;
3326 const char format_last_char = format[0] ? format[strlen(s: format) - 1] : 0;
3327 return (format_last_char == 'x' || format_last_char == 'X') ? ImGuiInputTextFlags_CharsHexadecimal : ImGuiInputTextFlags_CharsDecimal;
3328}
3329
3330// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3331// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3332// However this may not be ideal for all uses, as some user code may break on out of bound values.
3333bool 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)
3334{
3335 char fmt_buf[32];
3336 char data_buf[32];
3337 format = ImParseFormatTrimDecorations(fmt: format, buf: fmt_buf, IM_ARRAYSIZE(fmt_buf));
3338 DataTypeFormatString(buf: data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3339 ImStrTrimBlanks(str: data_buf);
3340
3341 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited;
3342 flags |= InputScalar_DefaultCharsFilter(data_type, format);
3343
3344 bool value_changed = false;
3345 if (TempInputText(bb, id, label, buf: data_buf, IM_ARRAYSIZE(data_buf), flags))
3346 {
3347 // Backup old value
3348 size_t data_type_size = DataTypeGetInfo(data_type)->Size;
3349 ImGuiDataTypeTempStorage data_backup;
3350 memcpy(dest: &data_backup, src: p_data, n: data_type_size);
3351
3352 // Apply new value (or operations) then clamp
3353 DataTypeApplyFromText(buf: data_buf, data_type, p_data, format);
3354 if (p_clamp_min || p_clamp_max)
3355 {
3356 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, arg_1: p_clamp_min, arg_2: p_clamp_max) > 0)
3357 ImSwap(a&: p_clamp_min, b&: p_clamp_max);
3358 DataTypeClamp(data_type, p_data, p_min: p_clamp_min, p_max: p_clamp_max);
3359 }
3360
3361 // Only mark as edited if new value is different
3362 value_changed = memcmp(s1: &data_backup, s2: p_data, n: data_type_size) != 0;
3363 if (value_changed)
3364 MarkItemEdited(id);
3365 }
3366 return value_changed;
3367}
3368
3369// 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.
3370// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3371bool 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)
3372{
3373 ImGuiWindow* window = GetCurrentWindow();
3374 if (window->SkipItems)
3375 return false;
3376
3377 ImGuiContext& g = *GImGui;
3378 ImGuiStyle& style = g.Style;
3379
3380 if (format == NULL)
3381 format = DataTypeGetInfo(data_type)->PrintFmt;
3382
3383 char buf[64];
3384 DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3385
3386 // Testing ActiveId as a minor optimization as filtering is not needed until active
3387 if (g.ActiveId == 0 && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0)
3388 flags |= InputScalar_DefaultCharsFilter(data_type, format);
3389 flags |= ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3390
3391 bool value_changed = false;
3392 if (p_step != NULL)
3393 {
3394 const float button_size = GetFrameHeight();
3395
3396 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3397 PushID(str_id: label);
3398 SetNextItemWidth(ImMax(lhs: 1.0f, rhs: CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3399 if (InputText(label: "", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3400 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
3401 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags);
3402
3403 // Step buttons
3404 const ImVec2 backup_frame_padding = style.FramePadding;
3405 style.FramePadding.x = style.FramePadding.y;
3406 ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups;
3407 if (flags & ImGuiInputTextFlags_ReadOnly)
3408 BeginDisabled();
3409 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3410 if (ButtonEx(label: "-", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3411 {
3412 DataTypeApplyOp(data_type, op: '-', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3413 value_changed = true;
3414 }
3415 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3416 if (ButtonEx(label: "+", size_arg: ImVec2(button_size, button_size), flags: button_flags))
3417 {
3418 DataTypeApplyOp(data_type, op: '+', output: p_data, arg1: p_data, arg2: g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3419 value_changed = true;
3420 }
3421 if (flags & ImGuiInputTextFlags_ReadOnly)
3422 EndDisabled();
3423
3424 const char* label_end = FindRenderedTextEnd(text: label);
3425 if (label != label_end)
3426 {
3427 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
3428 TextEx(text: label, text_end: label_end);
3429 }
3430 style.FramePadding = backup_frame_padding;
3431
3432 PopID();
3433 EndGroup();
3434 }
3435 else
3436 {
3437 if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3438 value_changed = DataTypeApplyFromText(buf, data_type, p_data, format);
3439 }
3440 if (value_changed)
3441 MarkItemEdited(id: g.LastItemData.ID);
3442
3443 return value_changed;
3444}
3445
3446bool 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)
3447{
3448 ImGuiWindow* window = GetCurrentWindow();
3449 if (window->SkipItems)
3450 return false;
3451
3452 ImGuiContext& g = *GImGui;
3453 bool value_changed = false;
3454 BeginGroup();
3455 PushID(str_id: label);
3456 PushMultiItemsWidths(components, width_full: CalcItemWidth());
3457 size_t type_size = GDataTypeInfo[data_type].Size;
3458 for (int i = 0; i < components; i++)
3459 {
3460 PushID(int_id: i);
3461 if (i > 0)
3462 SameLine(offset_from_start_x: 0, spacing: g.Style.ItemInnerSpacing.x);
3463 value_changed |= InputScalar(label: "", data_type, p_data, p_step, p_step_fast, format, flags);
3464 PopID();
3465 PopItemWidth();
3466 p_data = (void*)((char*)p_data + type_size);
3467 }
3468 PopID();
3469
3470 const char* label_end = FindRenderedTextEnd(text: label);
3471 if (label != label_end)
3472 {
3473 SameLine(offset_from_start_x: 0.0f, spacing: g.Style.ItemInnerSpacing.x);
3474 TextEx(text: label, text_end: label_end);
3475 }
3476
3477 EndGroup();
3478 return value_changed;
3479}
3480
3481bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3482{
3483 flags |= ImGuiInputTextFlags_CharsScientific;
3484 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);
3485}
3486
3487bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3488{
3489 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 2, NULL, NULL, format, flags);
3490}
3491
3492bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3493{
3494 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 3, NULL, NULL, format, flags);
3495}
3496
3497bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3498{
3499 return InputScalarN(label, data_type: ImGuiDataType_Float, p_data: v, components: 4, NULL, NULL, format, flags);
3500}
3501
3502bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3503{
3504 // 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.
3505 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3506 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);
3507}
3508
3509bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3510{
3511 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 2, NULL, NULL, format: "%d", flags);
3512}
3513
3514bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3515{
3516 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 3, NULL, NULL, format: "%d", flags);
3517}
3518
3519bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3520{
3521 return InputScalarN(label, data_type: ImGuiDataType_S32, p_data: v, components: 4, NULL, NULL, format: "%d", flags);
3522}
3523
3524bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3525{
3526 flags |= ImGuiInputTextFlags_CharsScientific;
3527 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);
3528}
3529
3530//-------------------------------------------------------------------------
3531// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3532//-------------------------------------------------------------------------
3533// - InputText()
3534// - InputTextWithHint()
3535// - InputTextMultiline()
3536// - InputTextGetCharInfo() [Internal]
3537// - InputTextReindexLines() [Internal]
3538// - InputTextReindexLinesRange() [Internal]
3539// - InputTextEx() [Internal]
3540// - DebugNodeInputTextState() [Internal]
3541//-------------------------------------------------------------------------
3542
3543bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3544{
3545 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3546 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3547}
3548
3549bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3550{
3551 return InputTextEx(label, NULL, buf, buf_size: (int)buf_size, size_arg: size, flags: flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3552}
3553
3554bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3555{
3556 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3557 return InputTextEx(label, hint, buf, buf_size: (int)buf_size, size_arg: ImVec2(0, 0), flags, callback, user_data);
3558}
3559
3560static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3561{
3562 int line_count = 0;
3563 const char* s = text_begin;
3564 while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding
3565 if (c == '\n')
3566 line_count++;
3567 s--;
3568 if (s[0] != '\n' && s[0] != '\r')
3569 line_count++;
3570 *out_text_end = s;
3571 return line_count;
3572}
3573
3574static ImVec2 InputTextCalcTextSizeW(ImGuiContext* ctx, const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line)
3575{
3576 ImGuiContext& g = *ctx;
3577 ImFont* font = g.Font;
3578 const float line_height = g.FontSize;
3579 const float scale = line_height / font->FontSize;
3580
3581 ImVec2 text_size = ImVec2(0, 0);
3582 float line_width = 0.0f;
3583
3584 const ImWchar* s = text_begin;
3585 while (s < text_end)
3586 {
3587 unsigned int c = (unsigned int)(*s++);
3588 if (c == '\n')
3589 {
3590 text_size.x = ImMax(lhs: text_size.x, rhs: line_width);
3591 text_size.y += line_height;
3592 line_width = 0.0f;
3593 if (stop_on_new_line)
3594 break;
3595 continue;
3596 }
3597 if (c == '\r')
3598 continue;
3599
3600 const float char_width = font->GetCharAdvance(c: (ImWchar)c) * scale;
3601 line_width += char_width;
3602 }
3603
3604 if (text_size.x < line_width)
3605 text_size.x = line_width;
3606
3607 if (out_offset)
3608 *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3609
3610 if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3611 text_size.y += line_height;
3612
3613 if (remaining)
3614 *remaining = s;
3615
3616 return text_size;
3617}
3618
3619// 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)
3620namespace ImStb
3621{
3622
3623static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->CurLenW; }
3624static ImWchar STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { return obj->TextW[idx]; }
3625static 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 STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); }
3626static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x200000 ? 0 : key; }
3627static ImWchar STB_TEXTEDIT_NEWLINE = '\n';
3628static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3629{
3630 const ImWchar* text = obj->TextW.Data;
3631 const ImWchar* text_remaining = NULL;
3632 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);
3633 r->x0 = 0.0f;
3634 r->x1 = size.x;
3635 r->baseline_y_delta = size.y;
3636 r->ymin = 0.0f;
3637 r->ymax = size.y;
3638 r->num_chars = (int)(text_remaining - (text + line_start_idx));
3639}
3640
3641// 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.
3642static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|' || c=='\n' || c=='\r'; }
3643static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (is_separator(c: obj->TextW[idx - 1]) && !is_separator(c: obj->TextW[idx]) ) : 1; }
3644static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) { if (obj->Flags & ImGuiInputTextFlags_Password) return 0; return idx > 0 ? (!is_separator(c: obj->TextW[idx - 1]) && is_separator(c: obj->TextW[idx])) : 1; }
3645static 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; }
3646static 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; }
3647static 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; }
3648static 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); }
3649#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
3650#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
3651
3652static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
3653{
3654 ImWchar* dst = obj->TextW.Data + pos;
3655
3656 // We maintain our buffer length in both UTF-8 and wchar formats
3657 obj->Edited = true;
3658 obj->CurLenA -= ImTextCountUtf8BytesFromStr(in_text: dst, in_text_end: dst + n);
3659 obj->CurLenW -= n;
3660
3661 // Offset remaining text (FIXME-OPT: Use memmove)
3662 const ImWchar* src = obj->TextW.Data + pos + n;
3663 while (ImWchar c = *src++)
3664 *dst++ = c;
3665 *dst = '\0';
3666}
3667
3668static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const ImWchar* new_text, int new_text_len)
3669{
3670 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
3671 const int text_len = obj->CurLenW;
3672 IM_ASSERT(pos <= text_len);
3673
3674 const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(in_text: new_text, in_text_end: new_text + new_text_len);
3675 if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA))
3676 return false;
3677
3678 // Grow internal buffer if needed
3679 if (new_text_len + text_len + 1 > obj->TextW.Size)
3680 {
3681 if (!is_resizable)
3682 return false;
3683 IM_ASSERT(text_len < obj->TextW.Size);
3684 obj->TextW.resize(new_size: text_len + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1);
3685 }
3686
3687 ImWchar* text = obj->TextW.Data;
3688 if (pos != text_len)
3689 memmove(dest: text + pos + new_text_len, src: text + pos, n: (size_t)(text_len - pos) * sizeof(ImWchar));
3690 memcpy(dest: text + pos, src: new_text, n: (size_t)new_text_len * sizeof(ImWchar));
3691
3692 obj->Edited = true;
3693 obj->CurLenW += new_text_len;
3694 obj->CurLenA += new_text_len_utf8;
3695 obj->TextW[obj->CurLenW] = '\0';
3696
3697 return true;
3698}
3699
3700// 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)
3701#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
3702#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
3703#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
3704#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
3705#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
3706#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
3707#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
3708#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
3709#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
3710#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
3711#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
3712#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
3713#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
3714#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
3715#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
3716#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
3717#define STB_TEXTEDIT_K_SHIFT 0x400000
3718
3719#define STB_TEXTEDIT_IMPLEMENTATION
3720#include "imstb_textedit.h"
3721
3722// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
3723// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
3724static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const STB_TEXTEDIT_CHARTYPE* text, int text_len)
3725{
3726 stb_text_makeundo_replace(str, state, where: 0, old_length: str->CurLenW, new_length: text_len);
3727 ImStb::STB_TEXTEDIT_DELETECHARS(obj: str, pos: 0, n: str->CurLenW);
3728 state->cursor = state->select_start = state->select_end = 0;
3729 if (text_len <= 0)
3730 return;
3731 if (ImStb::STB_TEXTEDIT_INSERTCHARS(obj: str, pos: 0, new_text: text, new_text_len: text_len))
3732 {
3733 state->cursor = state->select_start = state->select_end = text_len;
3734 state->has_preferred_x = 0;
3735 return;
3736 }
3737 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
3738}
3739
3740} // namespace ImStb
3741
3742void ImGuiInputTextState::OnKeyPressed(int key)
3743{
3744 stb_textedit_key(str: this, state: &Stb, key);
3745 CursorFollow = true;
3746 CursorAnimReset();
3747}
3748
3749ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
3750{
3751 memset(s: this, c: 0, n: sizeof(*this));
3752}
3753
3754// Public API to manipulate UTF-8 text
3755// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar)
3756// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
3757void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
3758{
3759 IM_ASSERT(pos + bytes_count <= BufTextLen);
3760 char* dst = Buf + pos;
3761 const char* src = Buf + pos + bytes_count;
3762 while (char c = *src++)
3763 *dst++ = c;
3764 *dst = '\0';
3765
3766 if (CursorPos >= pos + bytes_count)
3767 CursorPos -= bytes_count;
3768 else if (CursorPos >= pos)
3769 CursorPos = pos;
3770 SelectionStart = SelectionEnd = CursorPos;
3771 BufDirty = true;
3772 BufTextLen -= bytes_count;
3773}
3774
3775void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
3776{
3777 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
3778 const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(s: new_text);
3779 if (new_text_len + BufTextLen >= BufSize)
3780 {
3781 if (!is_resizable)
3782 return;
3783
3784 // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!)
3785 ImGuiContext& g = *GImGui;
3786 ImGuiInputTextState* edit_state = &g.InputTextState;
3787 IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
3788 IM_ASSERT(Buf == edit_state->TextA.Data);
3789 int new_buf_size = BufTextLen + ImClamp(v: new_text_len * 4, mn: 32, mx: ImMax(lhs: 256, rhs: new_text_len)) + 1;
3790 edit_state->TextA.reserve(new_capacity: new_buf_size + 1);
3791 Buf = edit_state->TextA.Data;
3792 BufSize = edit_state->BufCapacityA = new_buf_size;
3793 }
3794
3795 if (BufTextLen != pos)
3796 memmove(dest: Buf + pos + new_text_len, src: Buf + pos, n: (size_t)(BufTextLen - pos));
3797 memcpy(dest: Buf + pos, src: new_text, n: (size_t)new_text_len * sizeof(char));
3798 Buf[BufTextLen + new_text_len] = '\0';
3799
3800 if (CursorPos >= pos)
3801 CursorPos += new_text_len;
3802 SelectionStart = SelectionEnd = CursorPos;
3803 BufDirty = true;
3804 BufTextLen += new_text_len;
3805}
3806
3807// Return false to discard a character.
3808static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, ImGuiInputSource input_source)
3809{
3810 IM_ASSERT(input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Clipboard);
3811 unsigned int c = *p_char;
3812
3813 // Filter non-printable (NB: isprint is unreliable! see #2467)
3814 bool apply_named_filters = true;
3815 if (c < 0x20)
3816 {
3817 bool pass = false;
3818 pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code)
3819 pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput));
3820 if (!pass)
3821 return false;
3822 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
3823 }
3824
3825 if (input_source != ImGuiInputSource_Clipboard)
3826 {
3827 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
3828 if (c == 127)
3829 return false;
3830
3831 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
3832 if (c >= 0xE000 && c <= 0xF8FF)
3833 return false;
3834 }
3835
3836 // Filter Unicode ranges we are not handling in this build
3837 if (c > IM_UNICODE_CODEPOINT_MAX)
3838 return false;
3839
3840 // Generic named filters
3841 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific)))
3842 {
3843 // 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 '.'.
3844 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
3845 // 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.
3846 // Change the default decimal_point with:
3847 // ImGui::GetCurrentContext()->PlatformLocaleDecimalPoint = *localeconv()->decimal_point;
3848 // 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.
3849 ImGuiContext& g = *GImGui;
3850 const unsigned c_decimal_point = (unsigned int)g.PlatformLocaleDecimalPoint;
3851
3852 // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
3853 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
3854 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
3855 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
3856 if (c >= 0xFF01 && c <= 0xFF5E)
3857 c = c - 0xFF01 + 0x21;
3858
3859 // Allow 0-9 . - + * /
3860 if (flags & ImGuiInputTextFlags_CharsDecimal)
3861 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
3862 return false;
3863
3864 // Allow 0-9 . - + * / e E
3865 if (flags & ImGuiInputTextFlags_CharsScientific)
3866 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
3867 return false;
3868
3869 // Allow 0-9 a-F A-F
3870 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
3871 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
3872 return false;
3873
3874 // Turn a-z into A-Z
3875 if (flags & ImGuiInputTextFlags_CharsUppercase)
3876 if (c >= 'a' && c <= 'z')
3877 c += (unsigned int)('A' - 'a');
3878
3879 if (flags & ImGuiInputTextFlags_CharsNoBlank)
3880 if (ImCharIsBlankW(c))
3881 return false;
3882
3883 *p_char = c;
3884 }
3885
3886 // Custom callback filter
3887 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
3888 {
3889 ImGuiInputTextCallbackData callback_data;
3890 memset(s: &callback_data, c: 0, n: sizeof(ImGuiInputTextCallbackData));
3891 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
3892 callback_data.EventChar = (ImWchar)c;
3893 callback_data.Flags = flags;
3894 callback_data.UserData = user_data;
3895 if (callback(&callback_data) != 0)
3896 return false;
3897 *p_char = callback_data.EventChar;
3898 if (!callback_data.EventChar)
3899 return false;
3900 }
3901
3902 return true;
3903}
3904
3905// Find the shortest single replacement we can make to get the new text from the old text.
3906// Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end.
3907// 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.
3908static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
3909{
3910 ImGuiContext& g = *GImGui;
3911 const ImWchar* old_buf = state->TextW.Data;
3912 const int old_length = state->CurLenW;
3913 const int new_length = ImTextCountCharsFromUtf8(in_text: new_buf_a, in_text_end: new_buf_a + new_length_a);
3914 g.TempBuffer.reserve_discard(new_capacity: (new_length + 1) * sizeof(ImWchar));
3915 ImWchar* new_buf = (ImWchar*)(void*)g.TempBuffer.Data;
3916 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);
3917
3918 const int shorter_length = ImMin(lhs: old_length, rhs: new_length);
3919 int first_diff;
3920 for (first_diff = 0; first_diff < shorter_length; first_diff++)
3921 if (old_buf[first_diff] != new_buf[first_diff])
3922 break;
3923 if (first_diff == old_length && first_diff == new_length)
3924 return;
3925
3926 int old_last_diff = old_length - 1;
3927 int new_last_diff = new_length - 1;
3928 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
3929 if (old_buf[old_last_diff] != new_buf[new_last_diff])
3930 break;
3931
3932 const int insert_len = new_last_diff - first_diff + 1;
3933 const int delete_len = old_last_diff - first_diff + 1;
3934 if (insert_len > 0 || delete_len > 0)
3935 if (STB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(state: &state->Stb.undostate, pos: first_diff, insert_len: delete_len, delete_len: insert_len))
3936 for (int i = 0; i < delete_len; i++)
3937 p[i] = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: first_diff + i);
3938}
3939
3940// Edit a string of text
3941// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
3942// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
3943// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
3944// - 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.
3945// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
3946// (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
3947// 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)
3948bool 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)
3949{
3950 ImGuiWindow* window = GetCurrentWindow();
3951 if (window->SkipItems)
3952 return false;
3953
3954 IM_ASSERT(buf != NULL && buf_size >= 0);
3955 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
3956 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
3957
3958 ImGuiContext& g = *GImGui;
3959 ImGuiIO& io = g.IO;
3960 const ImGuiStyle& style = g.Style;
3961
3962 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
3963 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
3964 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
3965 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
3966 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
3967 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
3968 if (is_resizable)
3969 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
3970
3971 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
3972 BeginGroup();
3973 const ImGuiID id = window->GetID(str: label);
3974 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
3975 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
3976 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
3977
3978 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
3979 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
3980
3981 ImGuiWindow* draw_window = window;
3982 ImVec2 inner_size = frame_size;
3983 ImGuiItemStatusFlags item_status_flags = 0;
3984 ImGuiLastItemData item_data_backup;
3985 if (is_multiline)
3986 {
3987 ImVec2 backup_pos = window->DC.CursorPos;
3988 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
3989 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
3990 {
3991 EndGroup();
3992 return false;
3993 }
3994 item_status_flags = g.LastItemData.StatusFlags;
3995 item_data_backup = g.LastItemData;
3996 window->DC.CursorPos = backup_pos;
3997
3998 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
3999 // FIXME-NAV: Pressing NavActivate will trigger general child activation right before triggering our own below. Harmless but bizarre.
4000 PushStyleColor(idx: ImGuiCol_ChildBg, col: style.Colors[ImGuiCol_FrameBg]);
4001 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.FrameRounding);
4002 PushStyleVar(idx: ImGuiStyleVar_ChildBorderSize, val: style.FrameBorderSize);
4003 PushStyleVar(idx: ImGuiStyleVar_WindowPadding, val: ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4004 bool child_visible = BeginChildEx(name: label, id, size_arg: frame_bb.GetSize(), border: true, flags: ImGuiWindowFlags_NoMove);
4005 PopStyleVar(count: 3);
4006 PopStyleColor();
4007 if (!child_visible)
4008 {
4009 EndChild();
4010 EndGroup();
4011 return false;
4012 }
4013 draw_window = g.CurrentWindow; // Child window
4014 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.
4015 draw_window->DC.CursorPos += style.FramePadding;
4016 inner_size.x -= draw_window->ScrollbarSizes.x;
4017 }
4018 else
4019 {
4020 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4021 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
4022 if (!(flags & ImGuiInputTextFlags_MergedItem))
4023 if (!ItemAdd(bb: total_bb, id, nav_bb: &frame_bb, extra_flags: ImGuiItemFlags_Inputable))
4024 return false;
4025 item_status_flags = g.LastItemData.StatusFlags;
4026 }
4027 const bool hovered = ItemHoverable(bb: frame_bb, id);
4028 if (hovered)
4029 g.MouseCursor = ImGuiMouseCursor_TextInput;
4030
4031 // We are only allowed to access the state if we are already the active widget.
4032 ImGuiInputTextState* state = GetInputTextState(id);
4033
4034 const bool input_requested_by_tabbing = (item_status_flags & ImGuiItemStatusFlags_FocusedByTabbing) != 0;
4035 const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_Keyboard));
4036
4037 const bool user_clicked = hovered && io.MouseClicked[0];
4038 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4039 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(window: draw_window, axis: ImGuiAxis_Y);
4040 bool clear_active_id = false;
4041 bool select_all = false;
4042
4043 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4044
4045 const bool init_changed_specs = (state != NULL && state->Stb.single_line != !is_multiline);
4046 const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav || input_requested_by_tabbing);
4047 const bool init_state = (init_make_active || user_scroll_active);
4048 if ((init_state && g.ActiveId != id) || init_changed_specs)
4049 {
4050 // Access state even if we don't own it yet.
4051 state = &g.InputTextState;
4052 state->CursorAnimReset();
4053
4054 // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar)
4055 // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode)
4056 const int buf_len = (int)strlen(s: buf);
4057 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.
4058 memcpy(dest: state->InitialTextA.Data, src: buf, n: buf_len + 1);
4059
4060 // Preserve cursor position and undo/redo stack if we come back to same widget
4061 // FIXME: Since we reworked this on 2022/06, may want to differenciate recycle_cursor vs recycle_undostate?
4062 bool recycle_state = (state->ID == id && !init_changed_specs);
4063 if (recycle_state && (state->CurLenA != buf_len || (state->TextAIsValid && strncmp(s1: state->TextA.Data, s2: buf, n: buf_len) != 0)))
4064 recycle_state = false;
4065
4066 // Start edition
4067 const char* buf_end = NULL;
4068 state->ID = id;
4069 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.
4070 state->TextA.resize(new_size: 0);
4071 state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then)
4072 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: buf_size, in_text: buf, NULL, in_remaining: &buf_end);
4073 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.
4074
4075 if (recycle_state)
4076 {
4077 // Recycle existing cursor/selection/undo stack but clamp position
4078 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4079 state->CursorClamp();
4080 }
4081 else
4082 {
4083 state->ScrollX = 0.0f;
4084 stb_textedit_initialize_state(state: &state->Stb, is_single_line: !is_multiline);
4085 }
4086
4087 if (!is_multiline)
4088 {
4089 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4090 select_all = true;
4091 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4092 select_all = true;
4093 if (input_requested_by_tabbing || (user_clicked && io.KeyCtrl))
4094 select_all = true;
4095 }
4096
4097 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4098 state->Stb.insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4099 }
4100
4101 if (g.ActiveId != id && init_make_active)
4102 {
4103 IM_ASSERT(state && state->ID == id);
4104 SetActiveID(id, window);
4105 SetFocusID(id, window);
4106 FocusWindow(window);
4107 }
4108 if (g.ActiveId == id)
4109 {
4110 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4111 if (user_clicked)
4112 SetKeyOwner(key: ImGuiKey_MouseLeft, owner_id: id);
4113 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4114 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4115 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4116 SetKeyOwner(key: ImGuiKey_Home, owner_id: id);
4117 SetKeyOwner(key: ImGuiKey_End, owner_id: id);
4118 if (is_multiline)
4119 {
4120 SetKeyOwner(key: ImGuiKey_PageUp, owner_id: id);
4121 SetKeyOwner(key: ImGuiKey_PageDown, owner_id: id);
4122 }
4123 if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character.
4124 SetKeyOwner(key: ImGuiKey_Tab, owner_id: id);
4125 }
4126
4127 // 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)
4128 if (g.ActiveId == id && state == NULL)
4129 ClearActiveID();
4130
4131 // Release focus when we click outside
4132 if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4133 clear_active_id = true;
4134
4135 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4136 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4137 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4138 bool value_changed = false;
4139 bool validated = false;
4140
4141 // When read-only we always use the live data passed to the function
4142 // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :(
4143 if (is_readonly && state != NULL && (render_cursor || render_selection))
4144 {
4145 const char* buf_end = NULL;
4146 state->TextW.resize(new_size: buf_size + 1);
4147 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: buf, NULL, in_remaining: &buf_end);
4148 state->CurLenA = (int)(buf_end - buf);
4149 state->CursorClamp();
4150 render_selection &= state->HasSelection();
4151 }
4152
4153 // Select the buffer to render.
4154 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid;
4155 const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4156
4157 // Password pushes a temporary font with only a fallback glyph
4158 if (is_password && !is_displaying_hint)
4159 {
4160 const ImFontGlyph* glyph = g.Font->FindGlyph(c: '*');
4161 ImFont* password_font = &g.InputTextPasswordFont;
4162 password_font->FontSize = g.Font->FontSize;
4163 password_font->Scale = g.Font->Scale;
4164 password_font->Ascent = g.Font->Ascent;
4165 password_font->Descent = g.Font->Descent;
4166 password_font->ContainerAtlas = g.Font->ContainerAtlas;
4167 password_font->FallbackGlyph = glyph;
4168 password_font->FallbackAdvanceX = glyph->AdvanceX;
4169 IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
4170 PushFont(font: password_font);
4171 }
4172
4173 // Process mouse inputs and character inputs
4174 int backup_current_text_length = 0;
4175 if (g.ActiveId == id)
4176 {
4177 IM_ASSERT(state != NULL);
4178 backup_current_text_length = state->CurLenA;
4179 state->Edited = false;
4180 state->BufCapacityA = buf_size;
4181 state->Flags = flags;
4182
4183 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4184 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
4185 g.ActiveIdAllowOverlap = !io.MouseDown[0];
4186 g.WantTextInputNextFrame = 1;
4187
4188 // Edit in progress
4189 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX;
4190 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4191
4192 const bool is_osx = io.ConfigMacOSXBehaviors;
4193 if (select_all)
4194 {
4195 state->SelectAll();
4196 state->SelectedAllMouseLock = true;
4197 }
4198 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4199 {
4200 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4201 const int multiclick_count = (io.MouseClickedCount[0] - 2);
4202 if ((multiclick_count % 2) == 0)
4203 {
4204 // Double-click: Select word
4205 // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4206 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4207 const bool is_bol = (state->Stb.cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor - 1) == '\n';
4208 if (STB_TEXT_HAS_SELECTION(&state->Stb) || !is_bol)
4209 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4210 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4211 if (!STB_TEXT_HAS_SELECTION(&state->Stb))
4212 ImStb::stb_textedit_prep_selection_at_cursor(state: &state->Stb);
4213 state->Stb.cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj: state, idx: state->Stb.cursor);
4214 state->Stb.select_end = state->Stb.cursor;
4215 ImStb::stb_textedit_clamp(str: state, state: &state->Stb);
4216 }
4217 else
4218 {
4219 // Triple-click: Select line
4220 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(obj: state, idx: state->Stb.cursor) == '\n';
4221 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4222 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4223 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4224 if (!is_eol && is_multiline)
4225 {
4226 ImSwap(a&: state->Stb.select_start, b&: state->Stb.select_end);
4227 state->Stb.cursor = state->Stb.select_end;
4228 }
4229 state->CursorFollow = false;
4230 }
4231 state->CursorAnimReset();
4232 }
4233 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4234 {
4235 if (hovered)
4236 {
4237 if (io.KeyShift)
4238 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4239 else
4240 stb_textedit_click(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4241 state->CursorAnimReset();
4242 }
4243 }
4244 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4245 {
4246 stb_textedit_drag(str: state, state: &state->Stb, x: mouse_x, y: mouse_y);
4247 state->CursorAnimReset();
4248 state->CursorFollow = true;
4249 }
4250 if (state->SelectedAllMouseLock && !io.MouseDown[0])
4251 state->SelectedAllMouseLock = false;
4252
4253 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4254 // (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)
4255 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper);
4256 if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressed(key: ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly)
4257 {
4258 unsigned int c = '\t'; // Insert TAB
4259 if (InputTextFilterCharacter(p_char: &c, flags, callback, user_data: callback_user_data, input_source: ImGuiInputSource_Keyboard))
4260 state->OnKeyPressed(key: (int)c);
4261 }
4262
4263 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4264 // 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.
4265 if (io.InputQueueCharacters.Size > 0)
4266 {
4267 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4268 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4269 {
4270 // Insert character if they pass filtering
4271 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4272 if (c == '\t') // Skip Tab, see above.
4273 continue;
4274 if (InputTextFilterCharacter(p_char: &c, flags, callback, user_data: callback_user_data, input_source: ImGuiInputSource_Keyboard))
4275 state->OnKeyPressed(key: (int)c);
4276 }
4277
4278 // Consume characters
4279 io.InputQueueCharacters.resize(new_size: 0);
4280 }
4281 }
4282
4283 // Process other shortcuts/key-presses
4284 bool revert_edit = false;
4285 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4286 {
4287 IM_ASSERT(state != NULL);
4288
4289 const int row_count_per_page = ImMax(lhs: (int)((inner_size.y - style.FramePadding.y) / g.FontSize), rhs: 1);
4290 state->Stb.row_count_per_page = row_count_per_page;
4291
4292 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4293 const bool is_osx = io.ConfigMacOSXBehaviors;
4294 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4295 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
4296
4297 // 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)
4298 // Otherwise we could simply assume that we own the keys as we are active.
4299 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4300 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());
4301 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());
4302 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;
4303 const bool is_undo = (Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_Z, owner_id: id, flags: f_repeat)) && !is_readonly && is_undoable;
4304 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;
4305 const bool is_select_all = Shortcut(key_chord: ImGuiMod_Shortcut | ImGuiKey_A, owner_id: id);
4306
4307 // 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.
4308 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4309 const bool is_enter_pressed = IsKeyPressed(key: ImGuiKey_Enter, repeat: true) || IsKeyPressed(key: ImGuiKey_KeypadEnter, repeat: true);
4310 const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, repeat: false) || IsKeyPressed(ImGuiKey_NavGamepadInput, repeat: false));
4311 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));
4312
4313 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4314 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); }
4315 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); }
4316 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); }
4317 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); }
4318 else if (IsKeyPressed(key: ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4319 else if (IsKeyPressed(key: ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4320 else if (IsKeyPressed(key: ImGuiKey_Home)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4321 else if (IsKeyPressed(key: ImGuiKey_End)) { state->OnKeyPressed(key: io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4322 else if (IsKeyPressed(key: ImGuiKey_Delete) && !is_readonly && !is_cut) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); }
4323 else if (IsKeyPressed(key: ImGuiKey_Backspace) && !is_readonly)
4324 {
4325 if (!state->HasSelection())
4326 {
4327 if (is_wordmove_key_down)
4328 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4329 else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl)
4330 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4331 }
4332 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4333 }
4334 else if (is_enter_pressed || is_gamepad_validate)
4335 {
4336 // Determine if we turn Enter into a \n character
4337 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4338 if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4339 {
4340 validated = true;
4341 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4342 state->SelectAll(); // No need to scroll
4343 else
4344 clear_active_id = true;
4345 }
4346 else if (!is_readonly)
4347 {
4348 unsigned int c = '\n'; // Insert new line
4349 if (InputTextFilterCharacter(p_char: &c, flags, callback, user_data: callback_user_data, input_source: ImGuiInputSource_Keyboard))
4350 state->OnKeyPressed(key: (int)c);
4351 }
4352 }
4353 else if (is_cancel)
4354 {
4355 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4356 {
4357 if (state->CurLenA > 0)
4358 {
4359 revert_edit = true;
4360 }
4361 else
4362 {
4363 render_cursor = render_selection = false;
4364 clear_active_id = true;
4365 }
4366 }
4367 else
4368 {
4369 clear_active_id = revert_edit = true;
4370 render_cursor = render_selection = false;
4371 }
4372 }
4373 else if (is_undo || is_redo)
4374 {
4375 state->OnKeyPressed(key: is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4376 state->ClearSelection();
4377 }
4378 else if (is_select_all)
4379 {
4380 state->SelectAll();
4381 state->CursorFollow = true;
4382 }
4383 else if (is_cut || is_copy)
4384 {
4385 // Cut, Copy
4386 if (io.SetClipboardTextFn)
4387 {
4388 const int ib = state->HasSelection() ? ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : 0;
4389 const int ie = state->HasSelection() ? ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end) : state->CurLenW;
4390 const int clipboard_data_len = ImTextCountUtf8BytesFromStr(in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie) + 1;
4391 char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char));
4392 ImTextStrToUtf8(out_buf: clipboard_data, out_buf_size: clipboard_data_len, in_text: state->TextW.Data + ib, in_text_end: state->TextW.Data + ie);
4393 SetClipboardText(clipboard_data);
4394 MemFree(ptr: clipboard_data);
4395 }
4396 if (is_cut)
4397 {
4398 if (!state->HasSelection())
4399 state->SelectAll();
4400 state->CursorFollow = true;
4401 stb_textedit_cut(str: state, state: &state->Stb);
4402 }
4403 }
4404 else if (is_paste)
4405 {
4406 if (const char* clipboard = GetClipboardText())
4407 {
4408 // Filter pasted buffer
4409 const int clipboard_len = (int)strlen(s: clipboard);
4410 ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
4411 int clipboard_filtered_len = 0;
4412 for (const char* s = clipboard; *s; )
4413 {
4414 unsigned int c;
4415 s += ImTextCharFromUtf8(out_char: &c, in_text: s, NULL);
4416 if (c == 0)
4417 break;
4418 if (!InputTextFilterCharacter(p_char: &c, flags, callback, user_data: callback_user_data, input_source: ImGuiInputSource_Clipboard))
4419 continue;
4420 clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
4421 }
4422 clipboard_filtered[clipboard_filtered_len] = 0;
4423 if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
4424 {
4425 stb_textedit_paste(str: state, state: &state->Stb, ctext: clipboard_filtered, len: clipboard_filtered_len);
4426 state->CursorFollow = true;
4427 }
4428 MemFree(ptr: clipboard_filtered);
4429 }
4430 }
4431
4432 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4433 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4434 }
4435
4436 // Process callbacks and apply result back to user's buffer.
4437 const char* apply_new_text = NULL;
4438 int apply_new_text_length = 0;
4439 if (g.ActiveId == id)
4440 {
4441 IM_ASSERT(state != NULL);
4442 if (revert_edit && !is_readonly)
4443 {
4444 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4445 {
4446 // Clear input
4447 apply_new_text = "";
4448 apply_new_text_length = 0;
4449 STB_TEXTEDIT_CHARTYPE empty_string;
4450 stb_textedit_replace(str: state, state: &state->Stb, text: &empty_string, text_len: 0);
4451 }
4452 else if (strcmp(s1: buf, s2: state->InitialTextA.Data) != 0)
4453 {
4454 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
4455 // Push records into the undo stack so we can CTRL+Z the revert operation itself
4456 apply_new_text = state->InitialTextA.Data;
4457 apply_new_text_length = state->InitialTextA.Size - 1;
4458 ImVector<ImWchar> w_text;
4459 if (apply_new_text_length > 0)
4460 {
4461 w_text.resize(new_size: ImTextCountCharsFromUtf8(in_text: apply_new_text, in_text_end: apply_new_text + apply_new_text_length) + 1);
4462 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);
4463 }
4464 stb_textedit_replace(str: state, state: &state->Stb, text: w_text.Data, text_len: (apply_new_text_length > 0) ? (w_text.Size - 1) : 0);
4465 }
4466 }
4467
4468 // Apply ASCII value
4469 if (!is_readonly)
4470 {
4471 state->TextAIsValid = true;
4472 state->TextA.resize(new_size: state->TextW.Size * 4 + 1);
4473 ImTextStrToUtf8(out_buf: state->TextA.Data, out_buf_size: state->TextA.Size, in_text: state->TextW.Data, NULL);
4474 }
4475
4476 // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
4477 // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
4478 // This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
4479 const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
4480 if (apply_edit_back_to_user_buffer)
4481 {
4482 // Apply new value immediately - copy modified buffer back
4483 // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
4484 // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
4485 // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
4486
4487 // User callback
4488 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
4489 {
4490 IM_ASSERT(callback != NULL);
4491
4492 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
4493 ImGuiInputTextFlags event_flag = 0;
4494 ImGuiKey event_key = ImGuiKey_None;
4495 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressed(key: ImGuiKey_Tab))
4496 {
4497 event_flag = ImGuiInputTextFlags_CallbackCompletion;
4498 event_key = ImGuiKey_Tab;
4499 }
4500 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_UpArrow))
4501 {
4502 event_flag = ImGuiInputTextFlags_CallbackHistory;
4503 event_key = ImGuiKey_UpArrow;
4504 }
4505 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(key: ImGuiKey_DownArrow))
4506 {
4507 event_flag = ImGuiInputTextFlags_CallbackHistory;
4508 event_key = ImGuiKey_DownArrow;
4509 }
4510 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
4511 {
4512 event_flag = ImGuiInputTextFlags_CallbackEdit;
4513 }
4514 else if (flags & ImGuiInputTextFlags_CallbackAlways)
4515 {
4516 event_flag = ImGuiInputTextFlags_CallbackAlways;
4517 }
4518
4519 if (event_flag)
4520 {
4521 ImGuiInputTextCallbackData callback_data;
4522 memset(s: &callback_data, c: 0, n: sizeof(ImGuiInputTextCallbackData));
4523 callback_data.EventFlag = event_flag;
4524 callback_data.Flags = flags;
4525 callback_data.UserData = callback_user_data;
4526
4527 char* callback_buf = is_readonly ? buf : state->TextA.Data;
4528 callback_data.EventKey = event_key;
4529 callback_data.Buf = callback_buf;
4530 callback_data.BufTextLen = state->CurLenA;
4531 callback_data.BufSize = state->BufCapacityA;
4532 callback_data.BufDirty = false;
4533
4534 // 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)
4535 ImWchar* text = state->TextW.Data;
4536 const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.cursor);
4537 const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_start);
4538 const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(in_text: text, in_text_end: text + state->Stb.select_end);
4539
4540 // Call user code
4541 callback(&callback_data);
4542
4543 // Read back what user may have modified
4544 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
4545 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
4546 IM_ASSERT(callback_data.BufSize == state->BufCapacityA);
4547 IM_ASSERT(callback_data.Flags == flags);
4548 const bool buf_dirty = callback_data.BufDirty;
4549 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; }
4550 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); }
4551 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); }
4552 if (buf_dirty)
4553 {
4554 IM_ASSERT((flags & ImGuiInputTextFlags_ReadOnly) == 0);
4555 IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
4556 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() ?
4557 if (callback_data.BufTextLen > backup_current_text_length && is_resizable)
4558 state->TextW.resize(new_size: state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); // Worse case scenario resize
4559 state->CurLenW = ImTextStrFromUtf8(out_buf: state->TextW.Data, out_buf_size: state->TextW.Size, in_text: callback_data.Buf, NULL);
4560 state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
4561 state->CursorAnimReset();
4562 }
4563 }
4564 }
4565
4566 // Will copy result string if modified
4567 if (!is_readonly && strcmp(s1: state->TextA.Data, s2: buf) != 0)
4568 {
4569 apply_new_text = state->TextA.Data;
4570 apply_new_text_length = state->CurLenA;
4571 }
4572 }
4573 }
4574
4575 // Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
4576 if (apply_new_text != NULL)
4577 {
4578 // We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
4579 // of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
4580 // without any storage on user's side.
4581 IM_ASSERT(apply_new_text_length >= 0);
4582 if (is_resizable)
4583 {
4584 ImGuiInputTextCallbackData callback_data;
4585 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
4586 callback_data.Flags = flags;
4587 callback_data.Buf = buf;
4588 callback_data.BufTextLen = apply_new_text_length;
4589 callback_data.BufSize = ImMax(lhs: buf_size, rhs: apply_new_text_length + 1);
4590 callback_data.UserData = callback_user_data;
4591 callback(&callback_data);
4592 buf = callback_data.Buf;
4593 buf_size = callback_data.BufSize;
4594 apply_new_text_length = ImMin(lhs: callback_data.BufTextLen, rhs: buf_size - 1);
4595 IM_ASSERT(apply_new_text_length <= buf_size);
4596 }
4597 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
4598
4599 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
4600 ImStrncpy(dst: buf, src: apply_new_text, count: ImMin(lhs: apply_new_text_length + 1, rhs: buf_size));
4601 value_changed = true;
4602 }
4603
4604 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
4605 if (clear_active_id && g.ActiveId == id)
4606 ClearActiveID();
4607
4608 // Render frame
4609 if (!is_multiline)
4610 {
4611 RenderNavHighlight(bb: frame_bb, id);
4612 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
4613 }
4614
4615 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
4616 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
4617 ImVec2 text_size(0.0f, 0.0f);
4618
4619 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
4620 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
4621 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
4622 const int buf_display_max_length = 2 * 1024 * 1024;
4623 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
4624 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
4625 if (is_displaying_hint)
4626 {
4627 buf_display = hint;
4628 buf_display_end = hint + strlen(s: hint);
4629 }
4630
4631 // Render text. We currently only render selection when the widget is active or while scrolling.
4632 // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
4633 if (render_cursor || render_selection)
4634 {
4635 IM_ASSERT(state != NULL);
4636 if (!is_displaying_hint)
4637 buf_display_end = buf_display + state->CurLenA;
4638
4639 // Render text (with cursor and selection)
4640 // This is going to be messy. We need to:
4641 // - Display the text (this alone can be more easily clipped)
4642 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
4643 // - Measure text height (for scrollbar)
4644 // 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)
4645 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
4646 const ImWchar* text_begin = state->TextW.Data;
4647 ImVec2 cursor_offset, select_start_offset;
4648
4649 {
4650 // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions.
4651 const ImWchar* searches_input_ptr[2] = { NULL, NULL };
4652 int searches_result_line_no[2] = { -1000, -1000 };
4653 int searches_remaining = 0;
4654 if (render_cursor)
4655 {
4656 searches_input_ptr[0] = text_begin + state->Stb.cursor;
4657 searches_result_line_no[0] = -1;
4658 searches_remaining++;
4659 }
4660 if (render_selection)
4661 {
4662 searches_input_ptr[1] = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4663 searches_result_line_no[1] = -1;
4664 searches_remaining++;
4665 }
4666
4667 // Iterate all lines to find our line numbers
4668 // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter.
4669 searches_remaining += is_multiline ? 1 : 0;
4670 int line_count = 0;
4671 //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
4672 for (const ImWchar* s = text_begin; *s != 0; s++)
4673 if (*s == '\n')
4674 {
4675 line_count++;
4676 if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; }
4677 if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; }
4678 }
4679 line_count++;
4680 if (searches_result_line_no[0] == -1)
4681 searches_result_line_no[0] = line_count;
4682 if (searches_result_line_no[1] == -1)
4683 searches_result_line_no[1] = line_count;
4684
4685 // Calculate 2d position by finding the beginning of the line and measuring distance
4686 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;
4687 cursor_offset.y = searches_result_line_no[0] * g.FontSize;
4688 if (searches_result_line_no[1] >= 0)
4689 {
4690 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;
4691 select_start_offset.y = searches_result_line_no[1] * g.FontSize;
4692 }
4693
4694 // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
4695 if (is_multiline)
4696 text_size = ImVec2(inner_size.x, line_count * g.FontSize);
4697 }
4698
4699 // Scroll
4700 if (render_cursor && state->CursorFollow)
4701 {
4702 // Horizontal scroll in chunks of quarter width
4703 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
4704 {
4705 const float scroll_increment_x = inner_size.x * 0.25f;
4706 const float visible_width = inner_size.x - style.FramePadding.x;
4707 if (cursor_offset.x < state->ScrollX)
4708 state->ScrollX = IM_FLOOR(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
4709 else if (cursor_offset.x - visible_width >= state->ScrollX)
4710 state->ScrollX = IM_FLOOR(cursor_offset.x - visible_width + scroll_increment_x);
4711 }
4712 else
4713 {
4714 state->ScrollX = 0.0f;
4715 }
4716
4717 // Vertical scroll
4718 if (is_multiline)
4719 {
4720 // Test if cursor is vertically visible
4721 if (cursor_offset.y - g.FontSize < scroll_y)
4722 scroll_y = ImMax(lhs: 0.0f, rhs: cursor_offset.y - g.FontSize);
4723 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
4724 scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
4725 const float scroll_max_y = ImMax(lhs: (text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, rhs: 0.0f);
4726 scroll_y = ImClamp(v: scroll_y, mn: 0.0f, mx: scroll_max_y);
4727 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
4728 draw_window->Scroll.y = scroll_y;
4729 }
4730
4731 state->CursorFollow = false;
4732 }
4733
4734 // Draw selection
4735 const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f);
4736 if (render_selection)
4737 {
4738 const ImWchar* text_selected_begin = text_begin + ImMin(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4739 const ImWchar* text_selected_end = text_begin + ImMax(lhs: state->Stb.select_start, rhs: state->Stb.select_end);
4740
4741 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.
4742 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.
4743 float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
4744 ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
4745 for (const ImWchar* p = text_selected_begin; p < text_selected_end; )
4746 {
4747 if (rect_pos.y > clip_rect.w + g.FontSize)
4748 break;
4749 if (rect_pos.y < clip_rect.y)
4750 {
4751 //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bit
4752 //p = p ? p + 1 : text_selected_end;
4753 while (p < text_selected_end)
4754 if (*p++ == '\n')
4755 break;
4756 }
4757 else
4758 {
4759 ImVec2 rect_size = InputTextCalcTextSizeW(ctx: &g, text_begin: p, text_end: text_selected_end, remaining: &p, NULL, stop_on_new_line: true);
4760 if (rect_size.x <= 0.0f) rect_size.x = IM_FLOOR(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
4761 ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
4762 rect.ClipWith(r: clip_rect);
4763 if (rect.Overlaps(r: clip_rect))
4764 draw_window->DrawList->AddRectFilled(p_min: rect.Min, p_max: rect.Max, col: bg_color);
4765 }
4766 rect_pos.x = draw_pos.x - draw_scroll.x;
4767 rect_pos.y += g.FontSize;
4768 }
4769 }
4770
4771 // 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.
4772 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
4773 {
4774 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
4775 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);
4776 }
4777
4778 // Draw blinking cursor
4779 if (render_cursor)
4780 {
4781 state->CursorAnim += io.DeltaTime;
4782 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
4783 ImVec2 cursor_screen_pos = ImFloor(v: draw_pos + cursor_offset - draw_scroll);
4784 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);
4785 if (cursor_is_visible && cursor_screen_rect.Overlaps(r: clip_rect))
4786 draw_window->DrawList->AddLine(p1: cursor_screen_rect.Min, p2: cursor_screen_rect.GetBL(), col: GetColorU32(idx: ImGuiCol_Text));
4787
4788 // 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.)
4789 if (!is_readonly)
4790 {
4791 g.PlatformImeData.WantVisible = true;
4792 g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
4793 g.PlatformImeData.InputLineHeight = g.FontSize;
4794 g.PlatformImeViewport = window->Viewport->ID;
4795 }
4796 }
4797 }
4798 else
4799 {
4800 // Render text only (no selection, no cursor)
4801 if (is_multiline)
4802 text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(text_begin: buf_display, out_text_end: &buf_display_end) * g.FontSize); // We don't need width
4803 else if (!is_displaying_hint && g.ActiveId == id)
4804 buf_display_end = buf_display + state->CurLenA;
4805 else if (!is_displaying_hint)
4806 buf_display_end = buf_display + strlen(s: buf_display);
4807
4808 if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
4809 {
4810 ImU32 col = GetColorU32(idx: is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
4811 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);
4812 }
4813 }
4814
4815 if (is_password && !is_displaying_hint)
4816 PopFont();
4817
4818 if (is_multiline)
4819 {
4820 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (ref issue #4761)...
4821 Dummy(size: ImVec2(text_size.x, text_size.y + style.FramePadding.y));
4822 ImGuiItemFlags backup_item_flags = g.CurrentItemFlags;
4823 g.CurrentItemFlags |= ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
4824 EndChild();
4825 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
4826 g.CurrentItemFlags = backup_item_flags;
4827
4828 // ...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...
4829 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
4830 EndGroup();
4831 if (g.LastItemData.ID == 0)
4832 {
4833 g.LastItemData.ID = id;
4834 g.LastItemData.InFlags = item_data_backup.InFlags;
4835 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
4836 }
4837 }
4838
4839 // Log as text
4840 if (g.LogEnabled && (!is_password || is_displaying_hint))
4841 {
4842 LogSetNextTextDecoration(prefix: "{", suffix: "}");
4843 LogRenderedText(ref_pos: &draw_pos, text: buf_display, text_end: buf_display_end);
4844 }
4845
4846 if (label_size.x > 0)
4847 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), text: label);
4848
4849 if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited))
4850 MarkItemEdited(id);
4851
4852 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
4853 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
4854 return validated;
4855 else
4856 return value_changed;
4857}
4858
4859void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
4860{
4861#ifndef IMGUI_DISABLE_DEBUG_TOOLS
4862 ImGuiContext& g = *GImGui;
4863 ImStb::STB_TexteditState* stb_state = &state->Stb;
4864 ImStb::StbUndoState* undo_state = &stb_state->undostate;
4865 Text(fmt: "ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
4866 DebugLocateItemOnHover(target_id: state->ID);
4867 Text(fmt: "CurLenW: %d, CurLenA: %d, Cursor: %d, Selection: %d..%d", state->CurLenA, state->CurLenW, stb_state->cursor, stb_state->select_start, stb_state->select_end);
4868 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);
4869 if (BeginChild(str_id: "undopoints", size: ImVec2(0.0f, GetTextLineHeight() * 15), border: true)) // Visualize undo state
4870 {
4871 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(0, 0));
4872 for (int n = 0; n < STB_TEXTEDIT_UNDOSTATECOUNT; n++)
4873 {
4874 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
4875 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
4876 if (undo_rec_type == ' ')
4877 BeginDisabled();
4878 char buf[64] = "";
4879 if (undo_rec_type != ' ' && undo_rec->char_storage != -1)
4880 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);
4881 Text(fmt: "%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%s\"",
4882 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf);
4883 if (undo_rec_type == ' ')
4884 EndDisabled();
4885 }
4886 PopStyleVar();
4887 }
4888 EndChild();
4889#else
4890 IM_UNUSED(state);
4891#endif
4892}
4893
4894//-------------------------------------------------------------------------
4895// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
4896//-------------------------------------------------------------------------
4897// - ColorEdit3()
4898// - ColorEdit4()
4899// - ColorPicker3()
4900// - RenderColorRectWithAlphaCheckerboard() [Internal]
4901// - ColorPicker4()
4902// - ColorButton()
4903// - SetColorEditOptions()
4904// - ColorTooltip() [Internal]
4905// - ColorEditOptionsPopup() [Internal]
4906// - ColorPickerOptionsPopup() [Internal]
4907//-------------------------------------------------------------------------
4908
4909bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
4910{
4911 return ColorEdit4(label, col, flags: flags | ImGuiColorEditFlags_NoAlpha);
4912}
4913
4914// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
4915// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
4916static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
4917{
4918 // This check is optional. Suppose we have two color widgets side by side, both widgets display different colors, but both colors have hue and/or saturation undefined.
4919 // With color check: hue/saturation is preserved in one widget. Editing color in one widget would reset hue/saturation in another one.
4920 // Without color check: common hue/saturation would be displayed in all widgets that have hue/saturation undefined.
4921 // g.ColorEditLastColor is stored as ImU32 RGB value: this essentially gives us color equality check with reduced precision.
4922 // Tiny external color changes would not be detected and this check would still pass. This is OK, since we only restore hue/saturation _only_ if they are undefined,
4923 // therefore this change flipping hue/saturation from undefined to a very tiny value would still be represented in color picker.
4924 ImGuiContext& g = *GImGui;
4925 if (g.ColorEditLastColor != ImGui::ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
4926 return;
4927
4928 // When S == 0, H is undefined.
4929 // When H == 1 it wraps around to 0.
4930 if (*S == 0.0f || (*H == 0.0f && g.ColorEditLastHue == 1))
4931 *H = g.ColorEditLastHue;
4932
4933 // When V == 0, S is undefined.
4934 if (*V == 0.0f)
4935 *S = g.ColorEditLastSat;
4936}
4937
4938// Edit colors components (each component in 0.0f..1.0f range).
4939// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
4940// 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.
4941bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
4942{
4943 ImGuiWindow* window = GetCurrentWindow();
4944 if (window->SkipItems)
4945 return false;
4946
4947 ImGuiContext& g = *GImGui;
4948 const ImGuiStyle& style = g.Style;
4949 const float square_sz = GetFrameHeight();
4950 const float w_full = CalcItemWidth();
4951 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
4952 const float w_inputs = w_full - w_button;
4953 const char* label_display_end = FindRenderedTextEnd(text: label);
4954 g.NextItemData.ClearFlags();
4955
4956 BeginGroup();
4957 PushID(str_id: label);
4958
4959 // If we're not showing any slider there's no point in doing any HSV conversions
4960 const ImGuiColorEditFlags flags_untouched = flags;
4961 if (flags & ImGuiColorEditFlags_NoInputs)
4962 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
4963
4964 // Context menu: display and modify options (before defaults are applied)
4965 if (!(flags & ImGuiColorEditFlags_NoOptions))
4966 ColorEditOptionsPopup(col, flags);
4967
4968 // Read stored options
4969 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
4970 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
4971 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
4972 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
4973 if (!(flags & ImGuiColorEditFlags_PickerMask_))
4974 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
4975 if (!(flags & ImGuiColorEditFlags_InputMask_))
4976 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
4977 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
4978 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
4979 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
4980
4981 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
4982 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
4983 const int components = alpha ? 4 : 3;
4984
4985 // Convert to the formats we need
4986 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
4987 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
4988 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
4989 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
4990 {
4991 // Hue is lost when converting from greyscale rgb (saturation=0). Restore it.
4992 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
4993 ColorEditRestoreHS(col, H: &f[0], S: &f[1], V: &f[2]);
4994 }
4995 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]) };
4996
4997 bool value_changed = false;
4998 bool value_changed_as_float = false;
4999
5000 const ImVec2 pos = window->DC.CursorPos;
5001 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5002 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5003
5004 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5005 {
5006 // RGB/HSV 0..255 Sliders
5007 const float w_item_one = ImMax(lhs: 1.0f, IM_FLOOR((w_inputs - (style.ItemInnerSpacing.x) * (components - 1)) / (float)components));
5008 const float w_item_last = ImMax(lhs: 1.0f, IM_FLOOR(w_inputs - (w_item_one + style.ItemInnerSpacing.x) * (components - 1)));
5009
5010 const bool hide_prefix = (w_item_one <= CalcTextSize(text: (flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5011 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5012 static const char* fmt_table_int[3][4] =
5013 {
5014 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5015 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5016 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5017 };
5018 static const char* fmt_table_float[3][4] =
5019 {
5020 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5021 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5022 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5023 };
5024 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5025
5026 for (int n = 0; n < components; n++)
5027 {
5028 if (n > 0)
5029 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5030 SetNextItemWidth((n + 1 < components) ? w_item_one : w_item_last);
5031
5032 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5033 if (flags & ImGuiColorEditFlags_Float)
5034 {
5035 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]);
5036 value_changed_as_float |= value_changed;
5037 }
5038 else
5039 {
5040 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]);
5041 }
5042 if (!(flags & ImGuiColorEditFlags_NoOptions))
5043 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5044 }
5045 }
5046 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5047 {
5048 // RGB Hexadecimal Input
5049 char buf[64];
5050 if (alpha)
5051 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));
5052 else
5053 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));
5054 SetNextItemWidth(w_inputs);
5055 if (InputText(label: "##Text", buf, IM_ARRAYSIZE(buf), flags: ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase))
5056 {
5057 value_changed = true;
5058 char* p = buf;
5059 while (*p == '#' || ImCharIsBlankA(c: *p))
5060 p++;
5061 i[0] = i[1] = i[2] = 0;
5062 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5063 int r;
5064 if (alpha)
5065 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)
5066 else
5067 r = sscanf(s: p, format: "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5068 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5069 }
5070 if (!(flags & ImGuiColorEditFlags_NoOptions))
5071 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5072 }
5073
5074 ImGuiWindow* picker_active_window = NULL;
5075 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5076 {
5077 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5078 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5079
5080 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5081 if (ColorButton(desc_id: "##ColorButton", col: col_v4, flags))
5082 {
5083 if (!(flags & ImGuiColorEditFlags_NoPicker))
5084 {
5085 // Store current color and open a picker
5086 g.ColorPickerRef = col_v4;
5087 OpenPopup(str_id: "picker");
5088 SetNextWindowPos(pos: g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5089 }
5090 }
5091 if (!(flags & ImGuiColorEditFlags_NoOptions))
5092 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5093
5094 if (BeginPopup(str_id: "picker"))
5095 {
5096 if (g.CurrentWindow->BeginCount == 1)
5097 {
5098 picker_active_window = g.CurrentWindow;
5099 if (label != label_display_end)
5100 {
5101 TextEx(text: label, text_end: label_display_end);
5102 Spacing();
5103 }
5104 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5105 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5106 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5107 value_changed |= ColorPicker4(label: "##picker", col, flags: picker_flags, ref_col: &g.ColorPickerRef.x);
5108 }
5109 EndPopup();
5110 }
5111 }
5112
5113 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5114 {
5115 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5116 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5117 SameLine(offset_from_start_x: 0.0f, spacing: style.ItemInnerSpacing.x);
5118 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5119 TextEx(text: label, text_end: label_display_end);
5120 }
5121
5122 // Convert back
5123 if (value_changed && picker_active_window == NULL)
5124 {
5125 if (!value_changed_as_float)
5126 for (int n = 0; n < 4; n++)
5127 f[n] = i[n] / 255.0f;
5128 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5129 {
5130 g.ColorEditLastHue = f[0];
5131 g.ColorEditLastSat = f[1];
5132 ColorConvertHSVtoRGB(h: f[0], s: f[1], v: f[2], out_r&: f[0], out_g&: f[1], out_b&: f[2]);
5133 g.ColorEditLastColor = ColorConvertFloat4ToU32(in: ImVec4(f[0], f[1], f[2], 0));
5134 }
5135 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5136 ColorConvertRGBtoHSV(r: f[0], g: f[1], b: f[2], out_h&: f[0], out_s&: f[1], out_v&: f[2]);
5137
5138 col[0] = f[0];
5139 col[1] = f[1];
5140 col[2] = f[2];
5141 if (alpha)
5142 col[3] = f[3];
5143 }
5144
5145 PopID();
5146 EndGroup();
5147
5148 // Drag and Drop Target
5149 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5150 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5151 {
5152 bool accepted_drag_drop = false;
5153 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5154 {
5155 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5156 value_changed = accepted_drag_drop = true;
5157 }
5158 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5159 {
5160 memcpy(dest: (float*)col, src: payload->Data, n: sizeof(float) * components);
5161 value_changed = accepted_drag_drop = true;
5162 }
5163
5164 // Drag-drop payloads are always RGB
5165 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5166 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: col[0], out_s&: col[1], out_v&: col[2]);
5167 EndDragDropTarget();
5168 }
5169
5170 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5171 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5172 g.LastItemData.ID = g.ActiveId;
5173
5174 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5175 MarkItemEdited(id: g.LastItemData.ID);
5176
5177 return value_changed;
5178}
5179
5180bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5181{
5182 float col4[4] = { col[0], col[1], col[2], 1.0f };
5183 if (!ColorPicker4(label, col: col4, flags: flags | ImGuiColorEditFlags_NoAlpha))
5184 return false;
5185 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5186 return true;
5187}
5188
5189// Helper for ColorPicker4()
5190static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5191{
5192 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5193 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));
5194 ImGui::RenderArrowPointingAt(draw_list, pos: ImVec2(pos.x + half_sz.x, pos.y), half_sz, direction: ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5195 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));
5196 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));
5197}
5198
5199// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5200// (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.)
5201// 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..)
5202// 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)
5203bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5204{
5205 ImGuiContext& g = *GImGui;
5206 ImGuiWindow* window = GetCurrentWindow();
5207 if (window->SkipItems)
5208 return false;
5209
5210 ImDrawList* draw_list = window->DrawList;
5211 ImGuiStyle& style = g.Style;
5212 ImGuiIO& io = g.IO;
5213
5214 const float width = CalcItemWidth();
5215 g.NextItemData.ClearFlags();
5216
5217 PushID(str_id: label);
5218 BeginGroup();
5219
5220 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5221 flags |= ImGuiColorEditFlags_NoSmallPreview;
5222
5223 // Context menu: display and store options.
5224 if (!(flags & ImGuiColorEditFlags_NoOptions))
5225 ColorPickerOptionsPopup(ref_col: col, flags);
5226
5227 // Read stored options
5228 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5229 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5230 if (!(flags & ImGuiColorEditFlags_InputMask_))
5231 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5232 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5233 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5234 if (!(flags & ImGuiColorEditFlags_NoOptions))
5235 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5236
5237 // Setup
5238 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5239 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5240 ImVec2 picker_pos = window->DC.CursorPos;
5241 float square_sz = GetFrameHeight();
5242 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5243 float sv_picker_size = ImMax(lhs: bars_width * 1, rhs: width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5244 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5245 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5246 float bars_triangles_half_sz = IM_FLOOR(bars_width * 0.20f);
5247
5248 float backup_initial_col[4];
5249 memcpy(dest: backup_initial_col, src: col, n: components * sizeof(float));
5250
5251 float wheel_thickness = sv_picker_size * 0.08f;
5252 float wheel_r_outer = sv_picker_size * 0.50f;
5253 float wheel_r_inner = wheel_r_outer - wheel_thickness;
5254 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5255
5256 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5257 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5258 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5259 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5260 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5261
5262 float H = col[0], S = col[1], V = col[2];
5263 float R = col[0], G = col[1], B = col[2];
5264 if (flags & ImGuiColorEditFlags_InputRGB)
5265 {
5266 // Hue is lost when converting from greyscale rgb (saturation=0). Restore it.
5267 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5268 ColorEditRestoreHS(col, H: &H, S: &S, V: &V);
5269 }
5270 else if (flags & ImGuiColorEditFlags_InputHSV)
5271 {
5272 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5273 }
5274
5275 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5276
5277 PushItemFlag(option: ImGuiItemFlags_NoNav, enabled: true);
5278 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5279 {
5280 // Hue wheel + SV triangle logic
5281 InvisibleButton(str_id: "hsv", size_arg: ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5282 if (IsItemActive())
5283 {
5284 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5285 ImVec2 current_off = g.IO.MousePos - wheel_center;
5286 float initial_dist2 = ImLengthSqr(lhs: initial_off);
5287 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5288 {
5289 // Interactive with Hue wheel
5290 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5291 if (H < 0.0f)
5292 H += 1.0f;
5293 value_changed = value_changed_h = true;
5294 }
5295 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5296 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5297 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)))
5298 {
5299 // Interacting with SV triangle
5300 ImVec2 current_off_unrotated = ImRotate(v: current_off, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5301 if (!ImTriangleContainsPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated))
5302 current_off_unrotated = ImTriangleClosestPoint(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated);
5303 float uu, vv, ww;
5304 ImTriangleBarycentricCoords(a: triangle_pa, b: triangle_pb, c: triangle_pc, p: current_off_unrotated, out_u&: uu, out_v&: vv, out_w&: ww);
5305 V = ImClamp(v: 1.0f - vv, mn: 0.0001f, mx: 1.0f);
5306 S = ImClamp(v: uu / V, mn: 0.0001f, mx: 1.0f);
5307 value_changed = value_changed_sv = true;
5308 }
5309 }
5310 if (!(flags & ImGuiColorEditFlags_NoOptions))
5311 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5312 }
5313 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5314 {
5315 // SV rectangle logic
5316 InvisibleButton(str_id: "sv", size_arg: ImVec2(sv_picker_size, sv_picker_size));
5317 if (IsItemActive())
5318 {
5319 S = ImSaturate(f: (io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5320 V = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5321
5322 // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5323 if (g.ColorEditLastColor == ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0)))
5324 H = g.ColorEditLastHue;
5325 value_changed = value_changed_sv = true;
5326 }
5327 if (!(flags & ImGuiColorEditFlags_NoOptions))
5328 OpenPopupOnItemClick(str_id: "context", popup_flags: ImGuiPopupFlags_MouseButtonRight);
5329
5330 // Hue bar logic
5331 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5332 InvisibleButton(str_id: "hue", size_arg: ImVec2(bars_width, sv_picker_size));
5333 if (IsItemActive())
5334 {
5335 H = ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5336 value_changed = value_changed_h = true;
5337 }
5338 }
5339
5340 // Alpha bar logic
5341 if (alpha_bar)
5342 {
5343 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5344 InvisibleButton(str_id: "alpha", size_arg: ImVec2(bars_width, sv_picker_size));
5345 if (IsItemActive())
5346 {
5347 col[3] = 1.0f - ImSaturate(f: (io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5348 value_changed = true;
5349 }
5350 }
5351 PopItemFlag(); // ImGuiItemFlags_NoNav
5352
5353 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5354 {
5355 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5356 BeginGroup();
5357 }
5358
5359 if (!(flags & ImGuiColorEditFlags_NoLabel))
5360 {
5361 const char* label_display_end = FindRenderedTextEnd(text: label);
5362 if (label != label_display_end)
5363 {
5364 if ((flags & ImGuiColorEditFlags_NoSidePreview))
5365 SameLine(offset_from_start_x: 0, spacing: style.ItemInnerSpacing.x);
5366 TextEx(text: label, text_end: label_display_end);
5367 }
5368 }
5369
5370 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5371 {
5372 PushItemFlag(option: ImGuiItemFlags_NoNavDefaultFocus, enabled: true);
5373 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5374 if ((flags & ImGuiColorEditFlags_NoLabel))
5375 Text(fmt: "Current");
5376
5377 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
5378 ColorButton(desc_id: "##current", col: col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2));
5379 if (ref_col != NULL)
5380 {
5381 Text(fmt: "Original");
5382 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5383 if (ColorButton(desc_id: "##original", col: ref_col_v4, flags: (flags & sub_flags_to_forward), size: ImVec2(square_sz * 3, square_sz * 2)))
5384 {
5385 memcpy(dest: col, src: ref_col, n: components * sizeof(float));
5386 value_changed = true;
5387 }
5388 }
5389 PopItemFlag();
5390 EndGroup();
5391 }
5392
5393 // Convert back color to RGB
5394 if (value_changed_h || value_changed_sv)
5395 {
5396 if (flags & ImGuiColorEditFlags_InputRGB)
5397 {
5398 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: col[0], out_g&: col[1], out_b&: col[2]);
5399 g.ColorEditLastHue = H;
5400 g.ColorEditLastSat = S;
5401 g.ColorEditLastColor = ColorConvertFloat4ToU32(in: ImVec4(col[0], col[1], col[2], 0));
5402 }
5403 else if (flags & ImGuiColorEditFlags_InputHSV)
5404 {
5405 col[0] = H;
5406 col[1] = S;
5407 col[2] = V;
5408 }
5409 }
5410
5411 // R,G,B and H,S,V slider color editor
5412 bool value_changed_fix_hue_wrap = false;
5413 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5414 {
5415 PushItemWidth(item_width: (alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
5416 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
5417 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
5418 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5419 if (ColorEdit4(label: "##rgb", col, flags: sub_flags | ImGuiColorEditFlags_DisplayRGB))
5420 {
5421 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
5422 // 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)
5423 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
5424 value_changed = true;
5425 }
5426 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5427 value_changed |= ColorEdit4(label: "##hsv", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHSV);
5428 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5429 value_changed |= ColorEdit4(label: "##hex", col, flags: sub_flags | ImGuiColorEditFlags_DisplayHex);
5430 PopItemWidth();
5431 }
5432
5433 // Try to cancel hue wrap (after ColorEdit4 call), if any
5434 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
5435 {
5436 float new_H, new_S, new_V;
5437 ColorConvertRGBtoHSV(r: col[0], g: col[1], b: col[2], out_h&: new_H, out_s&: new_S, out_v&: new_V);
5438 if (new_H <= 0 && H > 0)
5439 {
5440 if (new_V <= 0 && V != new_V)
5441 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]);
5442 else if (new_S <= 0)
5443 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]);
5444 }
5445 }
5446
5447 if (value_changed)
5448 {
5449 if (flags & ImGuiColorEditFlags_InputRGB)
5450 {
5451 R = col[0];
5452 G = col[1];
5453 B = col[2];
5454 ColorConvertRGBtoHSV(r: R, g: G, b: B, out_h&: H, out_s&: S, out_v&: V);
5455 ColorEditRestoreHS(col, H: &H, S: &S, V: &V); // Fix local Hue as display below will use it immediately.
5456 }
5457 else if (flags & ImGuiColorEditFlags_InputHSV)
5458 {
5459 H = col[0];
5460 S = col[1];
5461 V = col[2];
5462 ColorConvertHSVtoRGB(h: H, s: S, v: V, out_r&: R, out_g&: G, out_b&: B);
5463 }
5464 }
5465
5466 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
5467 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
5468 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
5469 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
5470 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) };
5471
5472 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);
5473 ImU32 hue_color32 = ColorConvertFloat4ToU32(in: hue_color_f);
5474 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(in: ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
5475
5476 ImVec2 sv_cursor_pos;
5477
5478 if (flags & ImGuiColorEditFlags_PickerHueWheel)
5479 {
5480 // Render Hue Wheel
5481 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
5482 const int segment_per_arc = ImMax(lhs: 4, rhs: (int)wheel_r_outer / 12);
5483 for (int n = 0; n < 6; n++)
5484 {
5485 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
5486 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
5487 const int vert_start_idx = draw_list->VtxBuffer.Size;
5488 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);
5489 draw_list->PathStroke(col: col_white, flags: 0, thickness: wheel_thickness);
5490 const int vert_end_idx = draw_list->VtxBuffer.Size;
5491
5492 // Paint colors over existing vertices
5493 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
5494 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
5495 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0: col_hues[n], col1: col_hues[n + 1]);
5496 }
5497
5498 // Render Cursor + preview on Hue Wheel
5499 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
5500 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
5501 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);
5502 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
5503 int hue_cursor_segments = ImClamp(v: (int)(hue_cursor_rad / 1.4f), mn: 9, mx: 32);
5504 draw_list->AddCircleFilled(center: hue_cursor_pos, radius: hue_cursor_rad, col: hue_color32, num_segments: hue_cursor_segments);
5505 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad + 1, col: col_midgrey, num_segments: hue_cursor_segments);
5506 draw_list->AddCircle(center: hue_cursor_pos, radius: hue_cursor_rad, col: col_white, num_segments: hue_cursor_segments);
5507
5508 // Render SV triangle (rotated according to hue)
5509 ImVec2 tra = wheel_center + ImRotate(v: triangle_pa, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5510 ImVec2 trb = wheel_center + ImRotate(v: triangle_pb, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5511 ImVec2 trc = wheel_center + ImRotate(v: triangle_pc, cos_a: cos_hue_angle, sin_a: sin_hue_angle);
5512 ImVec2 uv_white = GetFontTexUvWhitePixel();
5513 draw_list->PrimReserve(idx_count: 6, vtx_count: 6);
5514 draw_list->PrimVtx(pos: tra, uv: uv_white, col: hue_color32);
5515 draw_list->PrimVtx(pos: trb, uv: uv_white, col: hue_color32);
5516 draw_list->PrimVtx(pos: trc, uv: uv_white, col: col_white);
5517 draw_list->PrimVtx(pos: tra, uv: uv_white, col: 0);
5518 draw_list->PrimVtx(pos: trb, uv: uv_white, col: col_black);
5519 draw_list->PrimVtx(pos: trc, uv: uv_white, col: 0);
5520 draw_list->AddTriangle(p1: tra, p2: trb, p3: trc, col: col_midgrey, thickness: 1.5f);
5521 sv_cursor_pos = ImLerp(a: ImLerp(a: trc, b: tra, t: ImSaturate(f: S)), b: trb, t: ImSaturate(f: 1 - V));
5522 }
5523 else if (flags & ImGuiColorEditFlags_PickerHueBar)
5524 {
5525 // Render SV Square
5526 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);
5527 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);
5528 RenderFrameBorder(p_min: picker_pos, p_max: picker_pos + ImVec2(sv_picker_size, sv_picker_size), rounding: 0.0f);
5529 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
5530 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);
5531
5532 // Render Hue Bar
5533 for (int i = 0; i < 6; ++i)
5534 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]);
5535 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
5536 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);
5537 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);
5538 }
5539
5540 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
5541 float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f;
5542 draw_list->AddCircleFilled(center: sv_cursor_pos, radius: sv_cursor_rad, col: user_col32_striped_of_alpha, num_segments: 12);
5543 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad + 1, col: col_midgrey, num_segments: 12);
5544 draw_list->AddCircle(center: sv_cursor_pos, radius: sv_cursor_rad, col: col_white, num_segments: 12);
5545
5546 // Render alpha bar
5547 if (alpha_bar)
5548 {
5549 float alpha = ImSaturate(f: col[3]);
5550 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
5551 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));
5552 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);
5553 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
5554 RenderFrameBorder(p_min: bar1_bb.Min, p_max: bar1_bb.Max, rounding: 0.0f);
5555 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);
5556 }
5557
5558 EndGroup();
5559
5560 if (value_changed && memcmp(s1: backup_initial_col, s2: col, n: components * sizeof(float)) == 0)
5561 value_changed = false;
5562 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5563 MarkItemEdited(id: g.LastItemData.ID);
5564
5565 PopID();
5566
5567 return value_changed;
5568}
5569
5570// A little color square. Return true when clicked.
5571// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
5572// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
5573// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
5574bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
5575{
5576 ImGuiWindow* window = GetCurrentWindow();
5577 if (window->SkipItems)
5578 return false;
5579
5580 ImGuiContext& g = *GImGui;
5581 const ImGuiID id = window->GetID(str: desc_id);
5582 const float default_size = GetFrameHeight();
5583 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
5584 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
5585 ItemSize(bb, text_baseline_y: (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
5586 if (!ItemAdd(bb, id))
5587 return false;
5588
5589 bool hovered, held;
5590 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held);
5591
5592 if (flags & ImGuiColorEditFlags_NoAlpha)
5593 flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
5594
5595 ImVec4 col_rgb = col;
5596 if (flags & ImGuiColorEditFlags_InputHSV)
5597 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);
5598
5599 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
5600 float grid_step = ImMin(lhs: size.x, rhs: size.y) / 2.99f;
5601 float rounding = ImMin(lhs: g.Style.FrameRounding, rhs: grid_step * 0.5f);
5602 ImRect bb_inner = bb;
5603 float off = 0.0f;
5604 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
5605 {
5606 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.
5607 bb_inner.Expand(amount: off);
5608 }
5609 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
5610 {
5611 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
5612 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);
5613 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);
5614 }
5615 else
5616 {
5617 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
5618 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha;
5619 if (col_source.w < 1.0f)
5620 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);
5621 else
5622 window->DrawList->AddRectFilled(p_min: bb_inner.Min, p_max: bb_inner.Max, col: GetColorU32(col: col_source), rounding);
5623 }
5624 RenderNavHighlight(bb, id);
5625 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
5626 {
5627 if (g.Style.FrameBorderSize > 0.0f)
5628 RenderFrameBorder(p_min: bb.Min, p_max: bb.Max, rounding);
5629 else
5630 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
5631 }
5632
5633 // Drag and Drop Source
5634 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
5635 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
5636 {
5637 if (flags & ImGuiColorEditFlags_NoAlpha)
5638 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, data: &col_rgb, sz: sizeof(float) * 3, cond: ImGuiCond_Once);
5639 else
5640 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, data: &col_rgb, sz: sizeof(float) * 4, cond: ImGuiCond_Once);
5641 ColorButton(desc_id, col, flags);
5642 SameLine();
5643 TextEx(text: "Color");
5644 EndDragDropSource();
5645 }
5646
5647 // Tooltip
5648 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered)
5649 ColorTooltip(text: desc_id, col: &col.x, flags: flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
5650
5651 return pressed;
5652}
5653
5654// Initialize/override default color options
5655void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
5656{
5657 ImGuiContext& g = *GImGui;
5658 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5659 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
5660 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
5661 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
5662 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
5663 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
5664 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
5665 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
5666 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
5667 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
5668 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
5669 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
5670 g.ColorEditOptions = flags;
5671}
5672
5673// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5674void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
5675{
5676 ImGuiContext& g = *GImGui;
5677
5678 BeginTooltipEx(tooltip_flags: ImGuiTooltipFlags_OverridePreviousTooltip, extra_window_flags: ImGuiWindowFlags_None);
5679 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
5680 if (text_end > text)
5681 {
5682 TextEx(text, text_end);
5683 Separator();
5684 }
5685
5686 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
5687 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5688 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]);
5689 ColorButton(desc_id: "##preview", col: cf, flags: (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, size_arg: sz);
5690 SameLine();
5691 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
5692 {
5693 if (flags & ImGuiColorEditFlags_NoAlpha)
5694 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]);
5695 else
5696 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]);
5697 }
5698 else if (flags & ImGuiColorEditFlags_InputHSV)
5699 {
5700 if (flags & ImGuiColorEditFlags_NoAlpha)
5701 Text(fmt: "H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
5702 else
5703 Text(fmt: "H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
5704 }
5705 EndTooltip();
5706}
5707
5708void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
5709{
5710 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
5711 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
5712 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup(str_id: "context"))
5713 return;
5714 ImGuiContext& g = *GImGui;
5715 ImGuiColorEditFlags opts = g.ColorEditOptions;
5716 if (allow_opt_inputs)
5717 {
5718 if (RadioButton(label: "RGB", active: (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
5719 if (RadioButton(label: "HSV", active: (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
5720 if (RadioButton(label: "Hex", active: (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
5721 }
5722 if (allow_opt_datatype)
5723 {
5724 if (allow_opt_inputs) Separator();
5725 if (RadioButton(label: "0..255", active: (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
5726 if (RadioButton(label: "0.00..1.00", active: (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
5727 }
5728
5729 if (allow_opt_inputs || allow_opt_datatype)
5730 Separator();
5731 if (Button(label: "Copy as..", size_arg: ImVec2(-1, 0)))
5732 OpenPopup(str_id: "Copy");
5733 if (BeginPopup(str_id: "Copy"))
5734 {
5735 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]);
5736 char buf[64];
5737 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5738 if (Selectable(label: buf))
5739 SetClipboardText(buf);
5740 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "(%d,%d,%d,%d)", cr, cg, cb, ca);
5741 if (Selectable(label: buf))
5742 SetClipboardText(buf);
5743 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X", cr, cg, cb);
5744 if (Selectable(label: buf))
5745 SetClipboardText(buf);
5746 if (!(flags & ImGuiColorEditFlags_NoAlpha))
5747 {
5748 ImFormatString(buf, IM_ARRAYSIZE(buf), fmt: "#%02X%02X%02X%02X", cr, cg, cb, ca);
5749 if (Selectable(label: buf))
5750 SetClipboardText(buf);
5751 }
5752 EndPopup();
5753 }
5754
5755 g.ColorEditOptions = opts;
5756 EndPopup();
5757}
5758
5759void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
5760{
5761 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
5762 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
5763 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup(str_id: "context"))
5764 return;
5765 ImGuiContext& g = *GImGui;
5766 if (allow_opt_picker)
5767 {
5768 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
5769 PushItemWidth(item_width: picker_size.x);
5770 for (int picker_type = 0; picker_type < 2; picker_type++)
5771 {
5772 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
5773 if (picker_type > 0) Separator();
5774 PushID(int_id: picker_type);
5775 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
5776 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
5777 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
5778 ImVec2 backup_pos = GetCursorScreenPos();
5779 if (Selectable(label: "##selectable", selected: false, flags: 0, size: picker_size)) // By default, Selectable() is closing popup
5780 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
5781 SetCursorScreenPos(backup_pos);
5782 ImVec4 previewing_ref_col;
5783 memcpy(dest: &previewing_ref_col, src: ref_col, n: sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
5784 ColorPicker4(label: "##previewing_picker", col: &previewing_ref_col.x, flags: picker_flags);
5785 PopID();
5786 }
5787 PopItemWidth();
5788 }
5789 if (allow_opt_alpha_bar)
5790 {
5791 if (allow_opt_picker) Separator();
5792 CheckboxFlags(label: "Alpha Bar", flags: &g.ColorEditOptions, flags_value: ImGuiColorEditFlags_AlphaBar);
5793 }
5794 EndPopup();
5795}
5796
5797//-------------------------------------------------------------------------
5798// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
5799//-------------------------------------------------------------------------
5800// - TreeNode()
5801// - TreeNodeV()
5802// - TreeNodeEx()
5803// - TreeNodeExV()
5804// - TreeNodeBehavior() [Internal]
5805// - TreePush()
5806// - TreePop()
5807// - GetTreeNodeToLabelSpacing()
5808// - SetNextItemOpen()
5809// - CollapsingHeader()
5810//-------------------------------------------------------------------------
5811
5812bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
5813{
5814 va_list args;
5815 va_start(args, fmt);
5816 bool is_open = TreeNodeExV(str_id, flags: 0, fmt, args);
5817 va_end(args);
5818 return is_open;
5819}
5820
5821bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
5822{
5823 va_list args;
5824 va_start(args, fmt);
5825 bool is_open = TreeNodeExV(ptr_id, flags: 0, fmt, args);
5826 va_end(args);
5827 return is_open;
5828}
5829
5830bool ImGui::TreeNode(const char* label)
5831{
5832 ImGuiWindow* window = GetCurrentWindow();
5833 if (window->SkipItems)
5834 return false;
5835 return TreeNodeBehavior(id: window->GetID(str: label), flags: 0, label, NULL);
5836}
5837
5838bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
5839{
5840 return TreeNodeExV(str_id, flags: 0, fmt, args);
5841}
5842
5843bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
5844{
5845 return TreeNodeExV(ptr_id, flags: 0, fmt, args);
5846}
5847
5848bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
5849{
5850 ImGuiWindow* window = GetCurrentWindow();
5851 if (window->SkipItems)
5852 return false;
5853
5854 return TreeNodeBehavior(id: window->GetID(str: label), flags, label, NULL);
5855}
5856
5857bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
5858{
5859 va_list args;
5860 va_start(args, fmt);
5861 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
5862 va_end(args);
5863 return is_open;
5864}
5865
5866bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
5867{
5868 va_list args;
5869 va_start(args, fmt);
5870 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
5871 va_end(args);
5872 return is_open;
5873}
5874
5875bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
5876{
5877 ImGuiWindow* window = GetCurrentWindow();
5878 if (window->SkipItems)
5879 return false;
5880
5881 const char* label, *label_end;
5882 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
5883 return TreeNodeBehavior(id: window->GetID(str: str_id), flags, label, label_end);
5884}
5885
5886bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
5887{
5888 ImGuiWindow* window = GetCurrentWindow();
5889 if (window->SkipItems)
5890 return false;
5891
5892 const char* label, *label_end;
5893 ImFormatStringToTempBufferV(out_buf: &label, out_buf_end: &label_end, fmt, args);
5894 return TreeNodeBehavior(id: window->GetID(ptr: ptr_id), flags, label, label_end);
5895}
5896
5897void ImGui::TreeNodeSetOpen(ImGuiID id, bool open)
5898{
5899 ImGuiContext& g = *GImGui;
5900 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
5901 storage->SetInt(key: id, val: open ? 1 : 0);
5902}
5903
5904bool ImGui::TreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags)
5905{
5906 if (flags & ImGuiTreeNodeFlags_Leaf)
5907 return true;
5908
5909 // We only write to the tree storage if the user clicks (or explicitly use the SetNextItemOpen function)
5910 ImGuiContext& g = *GImGui;
5911 ImGuiWindow* window = g.CurrentWindow;
5912 ImGuiStorage* storage = window->DC.StateStorage;
5913
5914 bool is_open;
5915 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen)
5916 {
5917 if (g.NextItemData.OpenCond & ImGuiCond_Always)
5918 {
5919 is_open = g.NextItemData.OpenVal;
5920 TreeNodeSetOpen(id, open: is_open);
5921 }
5922 else
5923 {
5924 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
5925 const int stored_value = storage->GetInt(key: id, default_val: -1);
5926 if (stored_value == -1)
5927 {
5928 is_open = g.NextItemData.OpenVal;
5929 TreeNodeSetOpen(id, open: is_open);
5930 }
5931 else
5932 {
5933 is_open = stored_value != 0;
5934 }
5935 }
5936 }
5937 else
5938 {
5939 is_open = storage->GetInt(key: id, default_val: (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
5940 }
5941
5942 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
5943 // NB- If we are above max depth we still allow manually opened nodes to be logged.
5944 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
5945 is_open = true;
5946
5947 return is_open;
5948}
5949
5950bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
5951{
5952 ImGuiWindow* window = GetCurrentWindow();
5953 if (window->SkipItems)
5954 return false;
5955
5956 ImGuiContext& g = *GImGui;
5957 const ImGuiStyle& style = g.Style;
5958 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
5959 const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(lhs: window->DC.CurrLineTextBaseOffset, rhs: style.FramePadding.y));
5960
5961 if (!label_end)
5962 label_end = FindRenderedTextEnd(text: label);
5963 const ImVec2 label_size = CalcTextSize(text: label, text_end: label_end, hide_text_after_double_hash: false);
5964
5965 // We vertically grow up to current line height up the typical widget height.
5966 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);
5967 ImRect frame_bb;
5968 frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
5969 frame_bb.Min.y = window->DC.CursorPos.y;
5970 frame_bb.Max.x = window->WorkRect.Max.x;
5971 frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
5972 if (display_frame)
5973 {
5974 // Framed header expand a little outside the default padding, to the edge of InnerClipRect
5975 // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f)
5976 frame_bb.Min.x -= IM_FLOOR(window->WindowPadding.x * 0.5f - 1.0f);
5977 frame_bb.Max.x += IM_FLOOR(window->WindowPadding.x * 0.5f);
5978 }
5979
5980 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapser arrow width + Spacing
5981 const float text_offset_y = ImMax(lhs: padding.y, rhs: window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
5982 const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x * 2 : 0.0f); // Include collapser
5983 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
5984 ItemSize(size: ImVec2(text_width, frame_height), text_baseline_y: padding.y);
5985
5986 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
5987 ImRect interact_bb = frame_bb;
5988 if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth)) == 0)
5989 interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f;
5990
5991 // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child.
5992 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
5993 // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero.
5994 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
5995 bool is_open = TreeNodeUpdateNextOpen(id, flags);
5996 if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
5997 window->DC.TreeJumpToParentOnPopMask |= (1 << window->DC.TreeDepth);
5998
5999 bool item_add = ItemAdd(bb: interact_bb, id);
6000 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6001 g.LastItemData.DisplayRect = frame_bb;
6002
6003 if (!item_add)
6004 {
6005 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6006 TreePushOverrideID(id);
6007 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6008 return is_open;
6009 }
6010
6011 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6012 if (flags & ImGuiTreeNodeFlags_AllowItemOverlap)
6013 button_flags |= ImGuiButtonFlags_AllowItemOverlap;
6014 if (!is_leaf)
6015 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6016
6017 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6018 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6019 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6020 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6021 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6022 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6023 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6024 button_flags |= ImGuiButtonFlags_NoKeyModifiers;
6025
6026 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6027 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6028 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6029 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6030 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6031 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6032 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6033 // It is rather standard that arrow click react on Down rather than Up.
6034 // 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.
6035 if (is_mouse_x_over_arrow)
6036 button_flags |= ImGuiButtonFlags_PressedOnClick;
6037 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6038 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6039 else
6040 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6041
6042 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6043 const bool was_selected = selected;
6044
6045 bool hovered, held;
6046 bool pressed = ButtonBehavior(bb: interact_bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6047 bool toggled = false;
6048 if (!is_leaf)
6049 {
6050 if (pressed && g.DragDropHoldJustPressedId != id)
6051 {
6052 if ((flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) == 0 || (g.NavActivateId == id))
6053 toggled = true;
6054 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6055 toggled |= is_mouse_x_over_arrow && !g.NavDisableMouseHover; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6056 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6057 toggled = true;
6058 }
6059 else if (pressed && g.DragDropHoldJustPressedId == id)
6060 {
6061 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6062 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6063 toggled = true;
6064 }
6065
6066 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6067 {
6068 toggled = true;
6069 NavMoveRequestCancel();
6070 }
6071 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?
6072 {
6073 toggled = true;
6074 NavMoveRequestCancel();
6075 }
6076
6077 if (toggled)
6078 {
6079 is_open = !is_open;
6080 window->DC.StateStorage->SetInt(key: id, val: is_open);
6081 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6082 }
6083 }
6084 if (flags & ImGuiTreeNodeFlags_AllowItemOverlap)
6085 SetItemAllowOverlap();
6086
6087 // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger.
6088 if (selected != was_selected) //-V547
6089 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6090
6091 // Render
6092 const ImU32 text_col = GetColorU32(idx: ImGuiCol_Text);
6093 ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin;
6094 if (display_frame)
6095 {
6096 // Framed type
6097 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6098 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: true, rounding: style.FrameRounding);
6099 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6100 if (flags & ImGuiTreeNodeFlags_Bullet)
6101 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);
6102 else if (!is_leaf)
6103 RenderArrow(draw_list: window->DrawList, pos: ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), col: text_col, dir: is_open ? ImGuiDir_Down : ImGuiDir_Right, scale: 1.0f);
6104 else // Leaf without bullet, left-adjusted text
6105 text_pos.x -= text_offset_x;
6106 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6107 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6108
6109 if (g.LogEnabled)
6110 LogSetNextTextDecoration(prefix: "###", suffix: "###");
6111 RenderTextClipped(pos_min: text_pos, pos_max: frame_bb.Max, text: label, text_end: label_end, text_size_if_known: &label_size);
6112 }
6113 else
6114 {
6115 // Unframed typed for tree nodes
6116 if (hovered || selected)
6117 {
6118 const ImU32 bg_col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6119 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: bg_col, border: false);
6120 }
6121 RenderNavHighlight(bb: frame_bb, id, flags: nav_highlight_flags);
6122 if (flags & ImGuiTreeNodeFlags_Bullet)
6123 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);
6124 else if (!is_leaf)
6125 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 ? ImGuiDir_Down : ImGuiDir_Right, scale: 0.70f);
6126 if (g.LogEnabled)
6127 LogSetNextTextDecoration(prefix: ">", NULL);
6128 RenderText(pos: text_pos, text: label, text_end: label_end, hide_text_after_hash: false);
6129 }
6130
6131 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6132 TreePushOverrideID(id);
6133 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6134 return is_open;
6135}
6136
6137void ImGui::TreePush(const char* str_id)
6138{
6139 ImGuiWindow* window = GetCurrentWindow();
6140 Indent();
6141 window->DC.TreeDepth++;
6142 PushID(str_id);
6143}
6144
6145void ImGui::TreePush(const void* ptr_id)
6146{
6147 ImGuiWindow* window = GetCurrentWindow();
6148 Indent();
6149 window->DC.TreeDepth++;
6150 PushID(ptr_id);
6151}
6152
6153void ImGui::TreePushOverrideID(ImGuiID id)
6154{
6155 ImGuiContext& g = *GImGui;
6156 ImGuiWindow* window = g.CurrentWindow;
6157 Indent();
6158 window->DC.TreeDepth++;
6159 PushOverrideID(id);
6160}
6161
6162void ImGui::TreePop()
6163{
6164 ImGuiContext& g = *GImGui;
6165 ImGuiWindow* window = g.CurrentWindow;
6166 Unindent();
6167
6168 window->DC.TreeDepth--;
6169 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6170
6171 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6172 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6173 if (g.NavIdIsAlive && (window->DC.TreeJumpToParentOnPopMask & tree_depth_mask))
6174 {
6175 SetNavID(id: window->IDStack.back(), nav_layer: g.NavLayer, focus_scope_id: 0, rect_rel: ImRect());
6176 NavMoveRequestCancel();
6177 }
6178 window->DC.TreeJumpToParentOnPopMask &= tree_depth_mask - 1;
6179
6180 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.
6181 PopID();
6182}
6183
6184// Horizontal distance preceding label when using TreeNode() or Bullet()
6185float ImGui::GetTreeNodeToLabelSpacing()
6186{
6187 ImGuiContext& g = *GImGui;
6188 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6189}
6190
6191// Set next TreeNode/CollapsingHeader open state.
6192void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6193{
6194 ImGuiContext& g = *GImGui;
6195 if (g.CurrentWindow->SkipItems)
6196 return;
6197 g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen;
6198 g.NextItemData.OpenVal = is_open;
6199 g.NextItemData.OpenCond = cond ? cond : ImGuiCond_Always;
6200}
6201
6202// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6203// 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().
6204bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6205{
6206 ImGuiWindow* window = GetCurrentWindow();
6207 if (window->SkipItems)
6208 return false;
6209
6210 return TreeNodeBehavior(id: window->GetID(str: label), flags: flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6211}
6212
6213// p_visible == NULL : regular collapsing header
6214// 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
6215// p_visible != NULL && *p_visible == false : do not show the header at all
6216// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6217bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6218{
6219 ImGuiWindow* window = GetCurrentWindow();
6220 if (window->SkipItems)
6221 return false;
6222
6223 if (p_visible && !*p_visible)
6224 return false;
6225
6226 ImGuiID id = window->GetID(str: label);
6227 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6228 if (p_visible)
6229 flags |= ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6230 bool is_open = TreeNodeBehavior(id, flags, label);
6231 if (p_visible != NULL)
6232 {
6233 // Create a small overlapping close button
6234 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6235 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6236 ImGuiContext& g = *GImGui;
6237 ImGuiLastItemData last_item_backup = g.LastItemData;
6238 float button_size = g.FontSize;
6239 float button_x = ImMax(lhs: g.LastItemData.Rect.Min.x, rhs: g.LastItemData.Rect.Max.x - g.Style.FramePadding.x * 2.0f - button_size);
6240 float button_y = g.LastItemData.Rect.Min.y;
6241 ImGuiID close_button_id = GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: id);
6242 if (CloseButton(id: close_button_id, pos: ImVec2(button_x, button_y)))
6243 *p_visible = false;
6244 g.LastItemData = last_item_backup;
6245 }
6246
6247 return is_open;
6248}
6249
6250//-------------------------------------------------------------------------
6251// [SECTION] Widgets: Selectable
6252//-------------------------------------------------------------------------
6253// - Selectable()
6254//-------------------------------------------------------------------------
6255
6256// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6257// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6258// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowItemOverlap are also frequently used flags.
6259// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6260bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6261{
6262 ImGuiWindow* window = GetCurrentWindow();
6263 if (window->SkipItems)
6264 return false;
6265
6266 ImGuiContext& g = *GImGui;
6267 const ImGuiStyle& style = g.Style;
6268
6269 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6270 ImGuiID id = window->GetID(str: label);
6271 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6272 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6273 ImVec2 pos = window->DC.CursorPos;
6274 pos.y += window->DC.CurrLineTextBaseOffset;
6275 ItemSize(size, text_baseline_y: 0.0f);
6276
6277 // Fill horizontal space
6278 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6279 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6280 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6281 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6282 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6283 size.x = ImMax(lhs: label_size.x, rhs: max_x - min_x);
6284
6285 // Text stays at the submission position, but bounding box may be extended on both sides
6286 const ImVec2 text_min = pos;
6287 const ImVec2 text_max(min_x + size.x, pos.y + size.y);
6288
6289 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
6290 ImRect bb(min_x, pos.y, text_max.x, text_max.y);
6291 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
6292 {
6293 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
6294 const float spacing_y = style.ItemSpacing.y;
6295 const float spacing_L = IM_FLOOR(spacing_x * 0.50f);
6296 const float spacing_U = IM_FLOOR(spacing_y * 0.50f);
6297 bb.Min.x -= spacing_L;
6298 bb.Min.y -= spacing_U;
6299 bb.Max.x += (spacing_x - spacing_L);
6300 bb.Max.y += (spacing_y - spacing_U);
6301 }
6302 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
6303
6304 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackground for every Selectable..
6305 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6306 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6307 if (span_all_columns)
6308 {
6309 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6310 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6311 }
6312
6313 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
6314 const bool item_add = ItemAdd(bb, id, NULL, extra_flags: disabled_item ? ImGuiItemFlags_Disabled : ImGuiItemFlags_None);
6315 if (span_all_columns)
6316 {
6317 window->ClipRect.Min.x = backup_clip_rect_min_x;
6318 window->ClipRect.Max.x = backup_clip_rect_max_x;
6319 }
6320
6321 if (!item_add)
6322 return false;
6323
6324 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
6325 if (disabled_item && !disabled_global) // Only testing this as an optimization
6326 BeginDisabled();
6327
6328 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
6329 // which would be advantageous since most selectable are not selected.
6330 if (span_all_columns && window->DC.CurrentColumns)
6331 PushColumnsBackground();
6332 else if (span_all_columns && g.CurrentTable)
6333 TablePushBackgroundChannel();
6334
6335 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
6336 ImGuiButtonFlags button_flags = 0;
6337 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
6338 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
6339 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
6340 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
6341 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
6342 if (flags & ImGuiSelectableFlags_AllowItemOverlap) { button_flags |= ImGuiButtonFlags_AllowItemOverlap; }
6343
6344 const bool was_selected = selected;
6345 bool hovered, held;
6346 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
6347
6348 // Auto-select when moved into
6349 // - This will be more fully fleshed in the range-select branch
6350 // - This is not exposed as it won't nicely work with some user side handling of shift/control
6351 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
6352 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
6353 // - (2) usage will fail with clipped items
6354 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
6355 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
6356 if (g.NavJustMovedToId == id)
6357 selected = pressed = true;
6358
6359 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard
6360 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
6361 {
6362 if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
6363 {
6364 SetNavID(id, nav_layer: window->DC.NavLayerCurrent, focus_scope_id: g.CurrentFocusScopeId, rect_rel: WindowRectAbsToRel(window, r: bb)); // (bb == NavRect)
6365 g.NavDisableHighlight = true;
6366 }
6367 }
6368 if (pressed)
6369 MarkItemEdited(id);
6370
6371 if (flags & ImGuiSelectableFlags_AllowItemOverlap)
6372 SetItemAllowOverlap();
6373
6374 // In this branch, Selectable() cannot toggle the selection so this will never trigger.
6375 if (selected != was_selected) //-V547
6376 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6377
6378 // Render
6379 if (held && (flags & ImGuiSelectableFlags_DrawHoveredWhenHeld))
6380 hovered = true;
6381 if (hovered || selected)
6382 {
6383 const ImU32 col = GetColorU32(idx: (held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6384 RenderFrame(p_min: bb.Min, p_max: bb.Max, fill_col: col, border: false, rounding: 0.0f);
6385 }
6386 RenderNavHighlight(bb, id, flags: ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding);
6387
6388 if (span_all_columns && window->DC.CurrentColumns)
6389 PopColumnsBackground();
6390 else if (span_all_columns && g.CurrentTable)
6391 TablePopBackgroundChannel();
6392
6393 RenderTextClipped(pos_min: text_min, pos_max: text_max, text: label, NULL, text_size_if_known: &label_size, align: style.SelectableTextAlign, clip_rect: &bb);
6394
6395 // Automatically close popups
6396 if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(g.LastItemData.InFlags & ImGuiItemFlags_SelectableDontClosePopup))
6397 CloseCurrentPopup();
6398
6399 if (disabled_item && !disabled_global)
6400 EndDisabled();
6401
6402 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
6403 return pressed; //-V1020
6404}
6405
6406bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6407{
6408 if (Selectable(label, selected: *p_selected, flags, size_arg))
6409 {
6410 *p_selected = !*p_selected;
6411 return true;
6412 }
6413 return false;
6414}
6415
6416//-------------------------------------------------------------------------
6417// [SECTION] Widgets: ListBox
6418//-------------------------------------------------------------------------
6419// - BeginListBox()
6420// - EndListBox()
6421// - ListBox()
6422//-------------------------------------------------------------------------
6423
6424// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
6425// 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).
6426bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
6427{
6428 ImGuiContext& g = *GImGui;
6429 ImGuiWindow* window = GetCurrentWindow();
6430 if (window->SkipItems)
6431 return false;
6432
6433 const ImGuiStyle& style = g.Style;
6434 const ImGuiID id = GetID(str_id: label);
6435 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6436
6437 // Size default to hold ~7.25 items.
6438 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
6439 ImVec2 size = ImFloor(v: CalcItemSize(size: size_arg, default_w: CalcItemWidth(), default_h: GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
6440 ImVec2 frame_size = ImVec2(size.x, ImMax(lhs: size.y, rhs: label_size.y));
6441 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
6442 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
6443 g.NextItemData.ClearFlags();
6444
6445 if (!IsRectVisible(rect_min: bb.Min, rect_max: bb.Max))
6446 {
6447 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
6448 ItemAdd(bb, id: 0, nav_bb: &frame_bb);
6449 return false;
6450 }
6451
6452 // FIXME-OPT: We could omit the BeginGroup() if label_size.x but would need to omit the EndGroup() as well.
6453 BeginGroup();
6454 if (label_size.x > 0.0f)
6455 {
6456 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
6457 RenderText(pos: label_pos, text: label);
6458 window->DC.CursorMaxPos = ImMax(lhs: window->DC.CursorMaxPos, rhs: label_pos + label_size);
6459 }
6460
6461 BeginChildFrame(id, size: frame_bb.GetSize());
6462 return true;
6463}
6464
6465#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
6466// OBSOLETED in 1.81 (from February 2021)
6467bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items)
6468{
6469 // If height_in_items == -1, default height is maximum 7.
6470 ImGuiContext& g = *GImGui;
6471 float height_in_items_f = (height_in_items < 0 ? ImMin(lhs: items_count, rhs: 7) : height_in_items) + 0.25f;
6472 ImVec2 size;
6473 size.x = 0.0f;
6474 size.y = GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f;
6475 return BeginListBox(label, size_arg: size);
6476}
6477#endif
6478
6479void ImGui::EndListBox()
6480{
6481 ImGuiContext& g = *GImGui;
6482 ImGuiWindow* window = g.CurrentWindow;
6483 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
6484 IM_UNUSED(window);
6485
6486 EndChildFrame();
6487 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
6488}
6489
6490bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
6491{
6492 const bool value_changed = ListBox(label, current_item, items_getter: Items_ArrayGetter, data: (void*)items, items_count, height_in_items: height_items);
6493 return value_changed;
6494}
6495
6496// This is merely a helper around BeginListBox(), EndListBox().
6497// Considering using those directly to submit custom data or store selection differently.
6498bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items)
6499{
6500 ImGuiContext& g = *GImGui;
6501
6502 // Calculate size from "height_in_items"
6503 if (height_in_items < 0)
6504 height_in_items = ImMin(lhs: items_count, rhs: 7);
6505 float height_in_items_f = height_in_items + 0.25f;
6506 ImVec2 size(0.0f, ImFloor(f: GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
6507
6508 if (!BeginListBox(label, size_arg: size))
6509 return false;
6510
6511 // Assume all items have even height (= 1 line of text). If you need items of different height,
6512 // you can create a custom version of ListBox() in your code without using the clipper.
6513 bool value_changed = false;
6514 ImGuiListClipper clipper;
6515 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.
6516 while (clipper.Step())
6517 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
6518 {
6519 const char* item_text;
6520 if (!items_getter(data, i, &item_text))
6521 item_text = "*Unknown item*";
6522
6523 PushID(int_id: i);
6524 const bool item_selected = (i == *current_item);
6525 if (Selectable(label: item_text, selected: item_selected))
6526 {
6527 *current_item = i;
6528 value_changed = true;
6529 }
6530 if (item_selected)
6531 SetItemDefaultFocus();
6532 PopID();
6533 }
6534 EndListBox();
6535
6536 if (value_changed)
6537 MarkItemEdited(id: g.LastItemData.ID);
6538
6539 return value_changed;
6540}
6541
6542//-------------------------------------------------------------------------
6543// [SECTION] Widgets: PlotLines, PlotHistogram
6544//-------------------------------------------------------------------------
6545// - PlotEx() [Internal]
6546// - PlotLines()
6547// - PlotHistogram()
6548//-------------------------------------------------------------------------
6549// Plot/Graph widgets are not very good.
6550// Consider writing your own, or using a third-party one, see:
6551// - ImPlot https://github.com/epezent/implot
6552// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
6553//-------------------------------------------------------------------------
6554
6555int 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, ImVec2 frame_size)
6556{
6557 ImGuiContext& g = *GImGui;
6558 ImGuiWindow* window = GetCurrentWindow();
6559 if (window->SkipItems)
6560 return -1;
6561
6562 const ImGuiStyle& style = g.Style;
6563 const ImGuiID id = window->GetID(str: label);
6564
6565 const ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
6566 if (frame_size.x == 0.0f)
6567 frame_size.x = CalcItemWidth();
6568 if (frame_size.y == 0.0f)
6569 frame_size.y = label_size.y + (style.FramePadding.y * 2);
6570
6571 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
6572 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
6573 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));
6574 ItemSize(bb: total_bb, text_baseline_y: style.FramePadding.y);
6575 if (!ItemAdd(bb: total_bb, id: 0, nav_bb: &frame_bb))
6576 return -1;
6577 const bool hovered = ItemHoverable(bb: frame_bb, id);
6578
6579 // Determine scale from values if not specified
6580 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
6581 {
6582 float v_min = FLT_MAX;
6583 float v_max = -FLT_MAX;
6584 for (int i = 0; i < values_count; i++)
6585 {
6586 const float v = values_getter(data, i);
6587 if (v != v) // Ignore NaN values
6588 continue;
6589 v_min = ImMin(lhs: v_min, rhs: v);
6590 v_max = ImMax(lhs: v_max, rhs: v);
6591 }
6592 if (scale_min == FLT_MAX)
6593 scale_min = v_min;
6594 if (scale_max == FLT_MAX)
6595 scale_max = v_max;
6596 }
6597
6598 RenderFrame(p_min: frame_bb.Min, p_max: frame_bb.Max, fill_col: GetColorU32(idx: ImGuiCol_FrameBg), border: true, rounding: style.FrameRounding);
6599
6600 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
6601 int idx_hovered = -1;
6602 if (values_count >= values_count_min)
6603 {
6604 int res_w = ImMin(lhs: (int)frame_size.x, rhs: values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
6605 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
6606
6607 // Tooltip on hover
6608 if (hovered && inner_bb.Contains(p: g.IO.MousePos))
6609 {
6610 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);
6611 const int v_idx = (int)(t * item_count);
6612 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
6613
6614 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
6615 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
6616 if (plot_type == ImGuiPlotType_Lines)
6617 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
6618 else if (plot_type == ImGuiPlotType_Histogram)
6619 SetTooltip("%d: %8.4g", v_idx, v0);
6620 idx_hovered = v_idx;
6621 }
6622
6623 const float t_step = 1.0f / (float)res_w;
6624 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
6625
6626 float v0 = values_getter(data, (0 + values_offset) % values_count);
6627 float t0 = 0.0f;
6628 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate(f: (v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
6629 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
6630
6631 const ImU32 col_base = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
6632 const ImU32 col_hovered = GetColorU32(idx: (plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
6633
6634 for (int n = 0; n < res_w; n++)
6635 {
6636 const float t1 = t0 + t_step;
6637 const int v1_idx = (int)(t0 * item_count + 0.5f);
6638 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
6639 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
6640 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate(f: (v1 - scale_min) * inv_scale) );
6641
6642 // 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.
6643 ImVec2 pos0 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: tp0);
6644 ImVec2 pos1 = ImLerp(a: inner_bb.Min, b: inner_bb.Max, t: (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
6645 if (plot_type == ImGuiPlotType_Lines)
6646 {
6647 window->DrawList->AddLine(p1: pos0, p2: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
6648 }
6649 else if (plot_type == ImGuiPlotType_Histogram)
6650 {
6651 if (pos1.x >= pos0.x + 2.0f)
6652 pos1.x -= 1.0f;
6653 window->DrawList->AddRectFilled(p_min: pos0, p_max: pos1, col: idx_hovered == v1_idx ? col_hovered : col_base);
6654 }
6655
6656 t0 = t1;
6657 tp0 = tp1;
6658 }
6659 }
6660
6661 // Text overlay
6662 if (overlay_text)
6663 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));
6664
6665 if (label_size.x > 0.0f)
6666 RenderText(pos: ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), text: label);
6667
6668 // Return hovered index or -1 if none are hovered.
6669 // 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().
6670 return idx_hovered;
6671}
6672
6673struct ImGuiPlotArrayGetterData
6674{
6675 const float* Values;
6676 int Stride;
6677
6678 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
6679};
6680
6681static float Plot_ArrayGetter(void* data, int idx)
6682{
6683 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
6684 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
6685 return v;
6686}
6687
6688void 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)
6689{
6690 ImGuiPlotArrayGetterData data(values, stride);
6691 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, frame_size: graph_size);
6692}
6693
6694void 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)
6695{
6696 PlotEx(plot_type: ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, frame_size: graph_size);
6697}
6698
6699void 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)
6700{
6701 ImGuiPlotArrayGetterData data(values, stride);
6702 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter: &Plot_ArrayGetter, data: (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, frame_size: graph_size);
6703}
6704
6705void 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)
6706{
6707 PlotEx(plot_type: ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, frame_size: graph_size);
6708}
6709
6710//-------------------------------------------------------------------------
6711// [SECTION] Widgets: Value helpers
6712// Those is not very useful, legacy API.
6713//-------------------------------------------------------------------------
6714// - Value()
6715//-------------------------------------------------------------------------
6716
6717void ImGui::Value(const char* prefix, bool b)
6718{
6719 Text(fmt: "%s: %s", prefix, (b ? "true" : "false"));
6720}
6721
6722void ImGui::Value(const char* prefix, int v)
6723{
6724 Text(fmt: "%s: %d", prefix, v);
6725}
6726
6727void ImGui::Value(const char* prefix, unsigned int v)
6728{
6729 Text(fmt: "%s: %d", prefix, v);
6730}
6731
6732void ImGui::Value(const char* prefix, float v, const char* float_format)
6733{
6734 if (float_format)
6735 {
6736 char fmt[64];
6737 ImFormatString(buf: fmt, IM_ARRAYSIZE(fmt), fmt: "%%s: %s", float_format);
6738 Text(fmt, prefix, v);
6739 }
6740 else
6741 {
6742 Text(fmt: "%s: %.3f", prefix, v);
6743 }
6744}
6745
6746//-------------------------------------------------------------------------
6747// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
6748//-------------------------------------------------------------------------
6749// - ImGuiMenuColumns [Internal]
6750// - BeginMenuBar()
6751// - EndMenuBar()
6752// - BeginMainMenuBar()
6753// - EndMainMenuBar()
6754// - BeginMenu()
6755// - EndMenu()
6756// - MenuItemEx() [Internal]
6757// - MenuItem()
6758//-------------------------------------------------------------------------
6759
6760// Helpers for internal use
6761void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
6762{
6763 if (window_reappearing)
6764 memset(s: Widths, c: 0, n: sizeof(Widths));
6765 Spacing = (ImU16)spacing;
6766 CalcNextTotalWidth(update_offsets: true);
6767 memset(s: Widths, c: 0, n: sizeof(Widths));
6768 TotalWidth = NextTotalWidth;
6769 NextTotalWidth = 0;
6770}
6771
6772void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
6773{
6774 ImU16 offset = 0;
6775 bool want_spacing = false;
6776 for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
6777 {
6778 ImU16 width = Widths[i];
6779 if (want_spacing && width > 0)
6780 offset += Spacing;
6781 want_spacing |= (width > 0);
6782 if (update_offsets)
6783 {
6784 if (i == 1) { OffsetLabel = offset; }
6785 if (i == 2) { OffsetShortcut = offset; }
6786 if (i == 3) { OffsetMark = offset; }
6787 }
6788 offset += width;
6789 }
6790 NextTotalWidth = offset;
6791}
6792
6793float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
6794{
6795 Widths[0] = ImMax(lhs: Widths[0], rhs: (ImU16)w_icon);
6796 Widths[1] = ImMax(lhs: Widths[1], rhs: (ImU16)w_label);
6797 Widths[2] = ImMax(lhs: Widths[2], rhs: (ImU16)w_shortcut);
6798 Widths[3] = ImMax(lhs: Widths[3], rhs: (ImU16)w_mark);
6799 CalcNextTotalWidth(update_offsets: false);
6800 return (float)ImMax(lhs: TotalWidth, rhs: NextTotalWidth);
6801}
6802
6803// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
6804// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
6805// 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.
6806// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
6807bool ImGui::BeginMenuBar()
6808{
6809 ImGuiWindow* window = GetCurrentWindow();
6810 if (window->SkipItems)
6811 return false;
6812 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
6813 return false;
6814
6815 IM_ASSERT(!window->DC.MenuBarAppending);
6816 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
6817 PushID(str_id: "##menubar");
6818
6819 // 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.
6820 // 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.
6821 ImRect bar_rect = window->MenuBarRect();
6822 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));
6823 clip_rect.ClipWith(r: window->OuterRectClipped);
6824 PushClipRect(clip_rect_min: clip_rect.Min, clip_rect_max: clip_rect.Max, intersect_with_current_clip_rect: false);
6825
6826 // 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).
6827 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
6828 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
6829 window->DC.IsSameLine = false;
6830 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
6831 window->DC.MenuBarAppending = true;
6832 AlignTextToFramePadding();
6833 return true;
6834}
6835
6836void ImGui::EndMenuBar()
6837{
6838 ImGuiWindow* window = GetCurrentWindow();
6839 if (window->SkipItems)
6840 return;
6841 ImGuiContext& g = *GImGui;
6842
6843 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
6844 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
6845 {
6846 // Try to find out if the request is for one of our child menu
6847 ImGuiWindow* nav_earliest_child = g.NavWindow;
6848 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
6849 nav_earliest_child = nav_earliest_child->ParentWindow;
6850 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
6851 {
6852 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
6853 // 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)
6854 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
6855 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
6856 FocusWindow(window);
6857 SetNavID(id: window->NavLastIds[layer], nav_layer: layer, focus_scope_id: 0, rect_rel: window->NavRectRel[layer]);
6858 g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection.
6859 g.NavDisableMouseHover = g.NavMousePosDirty = true;
6860 NavMoveRequestForward(move_dir: g.NavMoveDir, clip_dir: g.NavMoveClipDir, move_flags: g.NavMoveFlags, scroll_flags: g.NavMoveScrollFlags); // Repeat
6861 }
6862 }
6863
6864 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
6865 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
6866 IM_ASSERT(window->DC.MenuBarAppending);
6867 PopClipRect();
6868 PopID();
6869 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.
6870 g.GroupStack.back().EmitItem = false;
6871 EndGroup(); // Restore position on layer 0
6872 window->DC.LayoutType = ImGuiLayoutType_Vertical;
6873 window->DC.IsSameLine = false;
6874 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
6875 window->DC.MenuBarAppending = false;
6876}
6877
6878// Important: calling order matters!
6879// FIXME: Somehow overlapping with docking tech.
6880// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
6881bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
6882{
6883 IM_ASSERT(dir != ImGuiDir_None);
6884
6885 ImGuiWindow* bar_window = FindWindowByName(name);
6886 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
6887 if (bar_window == NULL || bar_window->BeginCount == 0)
6888 {
6889 // Calculate and set window size/position
6890 ImRect avail_rect = viewport->GetBuildWorkRect();
6891 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
6892 ImVec2 pos = avail_rect.Min;
6893 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
6894 pos[axis] = avail_rect.Max[axis] - axis_size;
6895 ImVec2 size = avail_rect.GetSize();
6896 size[axis] = axis_size;
6897 SetNextWindowPos(pos);
6898 SetNextWindowSize(size);
6899
6900 // Report our size into work area (for next frame) using actual window size
6901 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
6902 viewport->BuildWorkOffsetMin[axis] += axis_size;
6903 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
6904 viewport->BuildWorkOffsetMax[axis] -= axis_size;
6905 }
6906
6907 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
6908 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
6909 PushStyleVar(idx: ImGuiStyleVar_WindowRounding, val: 0.0f);
6910 PushStyleVar(idx: ImGuiStyleVar_WindowMinSize, val: ImVec2(0, 0)); // Lift normal size constraint
6911 bool is_open = Begin(name, NULL, flags: window_flags);
6912 PopStyleVar(count: 2);
6913
6914 return is_open;
6915}
6916
6917bool ImGui::BeginMainMenuBar()
6918{
6919 ImGuiContext& g = *GImGui;
6920 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
6921
6922 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
6923 SetCurrentViewport(NULL, viewport);
6924
6925 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
6926 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
6927 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
6928 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(lhs: g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, rhs: 0.0f));
6929 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
6930 float height = GetFrameHeight();
6931 bool is_open = BeginViewportSideBar(name: "##MainMenuBar", viewport_p: viewport, dir: ImGuiDir_Up, axis_size: height, window_flags);
6932 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
6933
6934 if (is_open)
6935 BeginMenuBar();
6936 else
6937 End();
6938 return is_open;
6939}
6940
6941void ImGui::EndMainMenuBar()
6942{
6943 EndMenuBar();
6944
6945 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
6946 // FIXME: With this strategy we won't be able to restore a NULL focus.
6947 ImGuiContext& g = *GImGui;
6948 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
6949 FocusTopMostWindowUnderOne(under_this_window: g.NavWindow, NULL);
6950
6951 End();
6952}
6953
6954static bool IsRootOfOpenMenuSet()
6955{
6956 ImGuiContext& g = *GImGui;
6957 ImGuiWindow* window = g.CurrentWindow;
6958 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
6959 return false;
6960
6961 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
6962 // (e.g. inside menu bar vs loose menu items) based on parent ID.
6963 // This would however prevent the use of e.g. PuhsID() user code submitting menus.
6964 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
6965 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
6966 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
6967 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
6968 // 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.
6969 // 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
6970 // 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
6971 // it likely won't be a problem anyone runs into.
6972 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
6973 return (window->DC.NavLayerCurrent == upper_popup->ParentNavLayer && upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu));
6974}
6975
6976bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
6977{
6978 ImGuiWindow* window = GetCurrentWindow();
6979 if (window->SkipItems)
6980 return false;
6981
6982 ImGuiContext& g = *GImGui;
6983 const ImGuiStyle& style = g.Style;
6984 const ImGuiID id = window->GetID(str: label);
6985 bool menu_is_open = IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None);
6986
6987 // 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)
6988 // 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.
6989 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
6990 if (window->Flags & ImGuiWindowFlags_ChildMenu)
6991 window_flags |= ImGuiWindowFlags_ChildWindow;
6992
6993 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
6994 // 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.
6995 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
6996 if (g.MenusIdSubmittedThisFrame.contains(v: id))
6997 {
6998 if (menu_is_open)
6999 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)
7000 else
7001 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
7002 return menu_is_open;
7003 }
7004
7005 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
7006 g.MenusIdSubmittedThisFrame.push_back(v: id);
7007
7008 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7009
7010 // 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)
7011 // This is only done for items for the menu set and not the full parent window.
7012 const bool menuset_is_open = IsRootOfOpenMenuSet();
7013 if (menuset_is_open)
7014 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
7015
7016 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
7017 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
7018 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
7019 ImVec2 popup_pos, pos = window->DC.CursorPos;
7020 PushID(str_id: label);
7021 if (!enabled)
7022 BeginDisabled();
7023 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
7024 bool pressed;
7025
7026 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
7027 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_DontClosePopups;
7028 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
7029 {
7030 // Menu inside an horizontal menu bar
7031 // Selectable extend their highlight by half ItemSpacing in each direction.
7032 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
7033 popup_pos = ImVec2(pos.x - 1.0f - IM_FLOOR(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight());
7034 window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f);
7035 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
7036 float w = label_size.x;
7037 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7038 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
7039 RenderText(pos: text_pos, text: label);
7040 PopStyleVar();
7041 window->DC.CursorPos.x += IM_FLOOR(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().
7042 }
7043 else
7044 {
7045 // Menu inside a regular/vertical menu
7046 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
7047 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
7048 popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
7049 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
7050 float checkmark_w = IM_FLOOR(g.FontSize * 1.20f);
7051 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
7052 float extra_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
7053 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7054 pressed = Selectable(label: "", selected: menu_is_open, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, 0.0f));
7055 RenderText(pos: text_pos, text: label);
7056 if (icon_w > 0.0f)
7057 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
7058 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);
7059 }
7060 if (!enabled)
7061 EndDisabled();
7062
7063 const bool hovered = (g.HoveredId == id) && enabled && !g.NavDisableMouseHover;
7064 if (menuset_is_open)
7065 PopItemFlag();
7066
7067 bool want_open = false;
7068 bool want_close = false;
7069 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
7070 {
7071 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
7072 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
7073 bool moving_toward_child_menu = false;
7074 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
7075 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
7076 if (g.HoveredWindow == window && child_menu_window != NULL)
7077 {
7078 float ref_unit = g.FontSize; // FIXME-DPI
7079 float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
7080 ImRect next_window_rect = child_menu_window->Rect();
7081 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
7082 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
7083 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
7084 float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, mn: ref_unit * 0.5f, mx: ref_unit * 2.5f); // add a bit of extra slack.
7085 ta.x += child_dir * -0.5f;
7086 tb.x += child_dir * ref_unit;
7087 tc.x += child_dir * ref_unit;
7088 tb.y = ta.y + ImMax(lhs: (tb.y - extra) - ta.y, rhs: -ref_unit * 8.0f); // triangle has maximum height to limit the slope and the bias toward large sub-menus
7089 tc.y = ta.y + ImMin(lhs: (tc.y + extra) - ta.y, rhs: +ref_unit * 8.0f);
7090 moving_toward_child_menu = ImTriangleContainsPoint(a: ta, b: tb, c: tc, p: g.IO.MousePos);
7091 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
7092 }
7093
7094 // 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)
7095 // 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.
7096 // (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.)
7097 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavDisableMouseHover)
7098 want_close = true;
7099
7100 // Open
7101 if (!menu_is_open && pressed) // Click/activate to open
7102 want_open = true;
7103 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
7104 want_open = true;
7105 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
7106 {
7107 want_open = true;
7108 NavMoveRequestCancel();
7109 }
7110 }
7111 else
7112 {
7113 // Menu bar
7114 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
7115 {
7116 want_close = true;
7117 want_open = menu_is_open = false;
7118 }
7119 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
7120 {
7121 want_open = true;
7122 }
7123 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
7124 {
7125 want_open = true;
7126 NavMoveRequestCancel();
7127 }
7128 }
7129
7130 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.. }'
7131 want_close = true;
7132 if (want_close && IsPopupOpen(id, popup_flags: ImGuiPopupFlags_None))
7133 ClosePopupToLevel(remaining: g.BeginPopupStack.Size, restore_focus_to_window_under_popup: true);
7134
7135 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
7136 PopID();
7137
7138 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
7139 {
7140 // Don't reopen/recycle same menu level in the same frame, first close the other menu and yield for a frame.
7141 OpenPopup(str_id: label);
7142 }
7143 else if (want_open)
7144 {
7145 menu_is_open = true;
7146 OpenPopup(str_id: label);
7147 }
7148
7149 if (menu_is_open)
7150 {
7151 ImGuiLastItemData last_item_in_parent = g.LastItemData;
7152 SetNextWindowPos(pos: popup_pos, cond: ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
7153 PushStyleVar(idx: ImGuiStyleVar_ChildRounding, val: style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
7154 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)
7155 PopStyleVar();
7156 if (menu_is_open)
7157 {
7158 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
7159 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
7160 g.LastItemData = last_item_in_parent;
7161 if (g.HoveredWindow == window)
7162 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
7163 }
7164 }
7165 else
7166 {
7167 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
7168 }
7169
7170 return menu_is_open;
7171}
7172
7173bool ImGui::BeginMenu(const char* label, bool enabled)
7174{
7175 return BeginMenuEx(label, NULL, enabled);
7176}
7177
7178void ImGui::EndMenu()
7179{
7180 // Nav: When a left move request our menu failed, close ourselves.
7181 ImGuiContext& g = *GImGui;
7182 ImGuiWindow* window = g.CurrentWindow;
7183 IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
7184 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
7185 if (window->BeginCount == window->BeginCountPreviousFrame)
7186 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
7187 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
7188 {
7189 ClosePopupToLevel(remaining: g.BeginPopupStack.Size - 1, restore_focus_to_window_under_popup: true);
7190 NavMoveRequestCancel();
7191 }
7192
7193 EndPopup();
7194}
7195
7196bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
7197{
7198 ImGuiWindow* window = GetCurrentWindow();
7199 if (window->SkipItems)
7200 return false;
7201
7202 ImGuiContext& g = *GImGui;
7203 ImGuiStyle& style = g.Style;
7204 ImVec2 pos = window->DC.CursorPos;
7205 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
7206
7207 // See BeginMenuEx() for comments about this.
7208 const bool menuset_is_open = IsRootOfOpenMenuSet();
7209 if (menuset_is_open)
7210 PushItemFlag(option: ImGuiItemFlags_NoWindowHoverableCheck, enabled: true);
7211
7212 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
7213 // 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.
7214 bool pressed;
7215 PushID(str_id: label);
7216 if (!enabled)
7217 BeginDisabled();
7218
7219 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
7220 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
7221 const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
7222 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
7223 {
7224 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
7225 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
7226 float w = label_size.x;
7227 window->DC.CursorPos.x += IM_FLOOR(style.ItemSpacing.x * 0.5f);
7228 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
7229 PushStyleVar(idx: ImGuiStyleVar_ItemSpacing, val: ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y));
7230 pressed = Selectable(label: "", selected, flags: selectable_flags, size_arg: ImVec2(w, 0.0f));
7231 PopStyleVar();
7232 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
7233 RenderText(pos: text_pos, text: label);
7234 window->DC.CursorPos.x += IM_FLOOR(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().
7235 }
7236 else
7237 {
7238 // Menu item inside a vertical menu
7239 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
7240 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
7241 float icon_w = (icon && icon[0]) ? CalcTextSize(text: icon, NULL).x : 0.0f;
7242 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(text: shortcut, NULL).x : 0.0f;
7243 float checkmark_w = IM_FLOOR(g.FontSize * 1.20f);
7244 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
7245 float stretch_w = ImMax(lhs: 0.0f, rhs: GetContentRegionAvail().x - min_w);
7246 pressed = Selectable(label: "", selected: false, flags: selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, size_arg: ImVec2(min_w, 0.0f));
7247 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
7248 {
7249 RenderText(pos: pos + ImVec2(offsets->OffsetLabel, 0.0f), text: label);
7250 if (icon_w > 0.0f)
7251 RenderText(pos: pos + ImVec2(offsets->OffsetIcon, 0.0f), text: icon);
7252 if (shortcut_w > 0.0f)
7253 {
7254 PushStyleColor(idx: ImGuiCol_Text, col: style.Colors[ImGuiCol_TextDisabled]);
7255 RenderText(pos: pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), text: shortcut, NULL, hide_text_after_hash: false);
7256 PopStyleColor();
7257 }
7258 if (selected)
7259 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);
7260 }
7261 }
7262 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
7263 if (!enabled)
7264 EndDisabled();
7265 PopID();
7266 if (menuset_is_open)
7267 PopItemFlag();
7268
7269 return pressed;
7270}
7271
7272bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
7273{
7274 return MenuItemEx(label, NULL, shortcut, selected, enabled);
7275}
7276
7277bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
7278{
7279 if (MenuItemEx(label, NULL, shortcut, selected: p_selected ? *p_selected : false, enabled))
7280 {
7281 if (p_selected)
7282 *p_selected = !*p_selected;
7283 return true;
7284 }
7285 return false;
7286}
7287
7288//-------------------------------------------------------------------------
7289// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
7290//-------------------------------------------------------------------------
7291// - BeginTabBar()
7292// - BeginTabBarEx() [Internal]
7293// - EndTabBar()
7294// - TabBarLayout() [Internal]
7295// - TabBarCalcTabID() [Internal]
7296// - TabBarCalcMaxTabWidth() [Internal]
7297// - TabBarFindTabById() [Internal]
7298// - TabBarAddTab() [Internal]
7299// - TabBarRemoveTab() [Internal]
7300// - TabBarCloseTab() [Internal]
7301// - TabBarScrollClamp() [Internal]
7302// - TabBarScrollToTab() [Internal]
7303// - TabBarQueueChangeTabOrder() [Internal]
7304// - TabBarScrollingButtons() [Internal]
7305// - TabBarTabListPopupButton() [Internal]
7306//-------------------------------------------------------------------------
7307
7308struct ImGuiTabBarSection
7309{
7310 int TabCount; // Number of tabs in this section.
7311 float Width; // Sum of width of tabs in this section (after shrinking down)
7312 float Spacing; // Horizontal spacing at the end of the section.
7313
7314 ImGuiTabBarSection() { memset(s: this, c: 0, n: sizeof(*this)); }
7315};
7316
7317namespace ImGui
7318{
7319 static void TabBarLayout(ImGuiTabBar* tab_bar);
7320 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
7321 static float TabBarCalcMaxTabWidth();
7322 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
7323 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
7324 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
7325 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
7326}
7327
7328ImGuiTabBar::ImGuiTabBar()
7329{
7330 memset(s: this, c: 0, n: sizeof(*this));
7331 CurrFrameVisible = PrevFrameVisible = -1;
7332 LastTabItemIdx = -1;
7333}
7334
7335static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
7336{
7337 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
7338}
7339
7340static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
7341{
7342 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
7343 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
7344 const int a_section = TabItemGetSectionIdx(tab: a);
7345 const int b_section = TabItemGetSectionIdx(tab: b);
7346 if (a_section != b_section)
7347 return a_section - b_section;
7348 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
7349}
7350
7351static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
7352{
7353 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
7354 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
7355 return (int)(a->BeginOrder - b->BeginOrder);
7356}
7357
7358static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
7359{
7360 ImGuiContext& g = *GImGui;
7361 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(n: ref.Index);
7362}
7363
7364static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
7365{
7366 ImGuiContext& g = *GImGui;
7367 if (g.TabBars.Contains(p: tab_bar))
7368 return ImGuiPtrOrIndex(g.TabBars.GetIndex(p: tab_bar));
7369 return ImGuiPtrOrIndex(tab_bar);
7370}
7371
7372bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
7373{
7374 ImGuiContext& g = *GImGui;
7375 ImGuiWindow* window = g.CurrentWindow;
7376 if (window->SkipItems)
7377 return false;
7378
7379 ImGuiID id = window->GetID(str: str_id);
7380 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(key: id);
7381 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);
7382 tab_bar->ID = id;
7383 return BeginTabBarEx(tab_bar, bb: tab_bar_bb, flags: flags | ImGuiTabBarFlags_IsFocused, NULL);
7384}
7385
7386bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags, ImGuiDockNode* dock_node)
7387{
7388 ImGuiContext& g = *GImGui;
7389 ImGuiWindow* window = g.CurrentWindow;
7390 if (window->SkipItems)
7391 return false;
7392
7393 if ((flags & ImGuiTabBarFlags_DockNode) == 0)
7394 PushOverrideID(id: tab_bar->ID);
7395
7396 // Add to stack
7397 g.CurrentTabBarStack.push_back(v: GetTabBarRefFromTabBar(tab_bar));
7398 g.CurrentTabBar = tab_bar;
7399
7400 // Append with multiple BeginTabBar()/EndTabBar() pairs.
7401 tab_bar->BackupCursorPos = window->DC.CursorPos;
7402 if (tab_bar->CurrFrameVisible == g.FrameCount)
7403 {
7404 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
7405 tab_bar->BeginCount++;
7406 return true;
7407 }
7408
7409 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
7410 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
7411 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
7412 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerByBeginOrder);
7413 tab_bar->TabsAddedNew = false;
7414
7415 // Flags
7416 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
7417 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
7418
7419 tab_bar->Flags = flags;
7420 tab_bar->BarRect = tab_bar_bb;
7421 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
7422 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
7423 tab_bar->CurrFrameVisible = g.FrameCount;
7424 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
7425 tab_bar->CurrTabsContentsHeight = 0.0f;
7426 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
7427 tab_bar->FramePadding = g.Style.FramePadding;
7428 tab_bar->TabsActiveCount = 0;
7429 tab_bar->BeginCount = 1;
7430
7431 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
7432 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
7433
7434 // Draw separator
7435 const ImU32 col = GetColorU32(idx: (flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive);
7436 const float y = tab_bar->BarRect.Max.y - 1.0f;
7437 if (dock_node != NULL)
7438 {
7439 const float separator_min_x = dock_node->Pos.x + window->WindowBorderSize;
7440 const float separator_max_x = dock_node->Pos.x + dock_node->Size.x - window->WindowBorderSize;
7441 window->DrawList->AddLine(p1: ImVec2(separator_min_x, y), p2: ImVec2(separator_max_x, y), col, thickness: 1.0f);
7442 }
7443 else
7444 {
7445 const float separator_min_x = tab_bar->BarRect.Min.x - IM_FLOOR(window->WindowPadding.x * 0.5f);
7446 const float separator_max_x = tab_bar->BarRect.Max.x + IM_FLOOR(window->WindowPadding.x * 0.5f);
7447 window->DrawList->AddLine(p1: ImVec2(separator_min_x, y), p2: ImVec2(separator_max_x, y), col, thickness: 1.0f);
7448 }
7449 return true;
7450}
7451
7452void ImGui::EndTabBar()
7453{
7454 ImGuiContext& g = *GImGui;
7455 ImGuiWindow* window = g.CurrentWindow;
7456 if (window->SkipItems)
7457 return;
7458
7459 ImGuiTabBar* tab_bar = g.CurrentTabBar;
7460 if (tab_bar == NULL)
7461 {
7462 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
7463 return;
7464 }
7465
7466 // Fallback in case no TabItem have been submitted
7467 if (tab_bar->WantLayout)
7468 TabBarLayout(tab_bar);
7469
7470 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
7471 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
7472 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
7473 {
7474 tab_bar->CurrTabsContentsHeight = ImMax(lhs: window->DC.CursorPos.y - tab_bar->BarRect.Max.y, rhs: tab_bar->CurrTabsContentsHeight);
7475 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
7476 }
7477 else
7478 {
7479 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
7480 }
7481 if (tab_bar->BeginCount > 1)
7482 window->DC.CursorPos = tab_bar->BackupCursorPos;
7483
7484 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
7485 PopID();
7486
7487 g.CurrentTabBarStack.pop_back();
7488 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(ref: g.CurrentTabBarStack.back());
7489}
7490
7491// This is called only once a frame before by the first call to ItemTab()
7492// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
7493static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
7494{
7495 ImGuiContext& g = *GImGui;
7496 tab_bar->WantLayout = false;
7497
7498 // Garbage collect by compacting list
7499 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
7500 int tab_dst_n = 0;
7501 bool need_sort_by_section = false;
7502 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
7503 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
7504 {
7505 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
7506 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
7507 {
7508 // Remove tab
7509 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
7510 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
7511 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
7512 continue;
7513 }
7514 if (tab_dst_n != tab_src_n)
7515 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
7516
7517 tab = &tab_bar->Tabs[tab_dst_n];
7518 tab->IndexDuringLayout = (ImS16)tab_dst_n;
7519
7520 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
7521 int curr_tab_section_n = TabItemGetSectionIdx(tab);
7522 if (tab_dst_n > 0)
7523 {
7524 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
7525 int prev_tab_section_n = TabItemGetSectionIdx(tab: prev_tab);
7526 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
7527 need_sort_by_section = true;
7528 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
7529 need_sort_by_section = true;
7530 }
7531
7532 sections[curr_tab_section_n].TabCount++;
7533 tab_dst_n++;
7534 }
7535 if (tab_bar->Tabs.Size != tab_dst_n)
7536 tab_bar->Tabs.resize(new_size: tab_dst_n);
7537
7538 if (need_sort_by_section)
7539 ImQsort(base: tab_bar->Tabs.Data, count: tab_bar->Tabs.Size, size_of_element: sizeof(ImGuiTabItem), compare_func: TabItemComparerBySection);
7540
7541 // Calculate spacing between sections
7542 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
7543 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
7544
7545 // Setup next selected tab
7546 ImGuiID scroll_to_tab_id = 0;
7547 if (tab_bar->NextSelectedTabId)
7548 {
7549 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
7550 tab_bar->NextSelectedTabId = 0;
7551 scroll_to_tab_id = tab_bar->SelectedTabId;
7552 }
7553
7554 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
7555 if (tab_bar->ReorderRequestTabId != 0)
7556 {
7557 if (TabBarProcessReorder(tab_bar))
7558 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
7559 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
7560 tab_bar->ReorderRequestTabId = 0;
7561 }
7562
7563 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
7564 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
7565 if (tab_list_popup_button)
7566 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
7567 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
7568
7569 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
7570 // (whereas our tabs are stored as: leading, central, trailing)
7571 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
7572 g.ShrinkWidthBuffer.resize(new_size: tab_bar->Tabs.Size);
7573
7574 // Compute ideal tabs widths + store them into shrink buffer
7575 ImGuiTabItem* most_recently_selected_tab = NULL;
7576 int curr_section_n = -1;
7577 bool found_selected_tab_id = false;
7578 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
7579 {
7580 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
7581 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
7582
7583 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
7584 most_recently_selected_tab = tab;
7585 if (tab->ID == tab_bar->SelectedTabId)
7586 found_selected_tab_id = true;
7587 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
7588 scroll_to_tab_id = tab->ID;
7589
7590 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
7591 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
7592 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
7593 const char* tab_name = tab_bar->GetTabName(tab);
7594 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
7595 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(label: tab_name, has_close_button_or_unsaved_marker).x;
7596
7597 int section_n = TabItemGetSectionIdx(tab);
7598 ImGuiTabBarSection* section = &sections[section_n];
7599 section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
7600 curr_section_n = section_n;
7601
7602 // Store data so we can build an array sorted by width if we need to shrink tabs down
7603 IM_MSVC_WARNING_SUPPRESS(6385);
7604 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
7605 shrink_width_item->Index = tab_n;
7606 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
7607 tab->Width = ImMax(lhs: tab->ContentWidth, rhs: 1.0f);
7608 }
7609
7610 // Compute total ideal width (used for e.g. auto-resizing a window)
7611 tab_bar->WidthAllTabsIdeal = 0.0f;
7612 for (int section_n = 0; section_n < 3; section_n++)
7613 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
7614
7615 // Horizontal scrolling buttons
7616 // (note that TabBarScrollButtons() will alter BarRect.Max.x)
7617 if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
7618 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
7619 {
7620 scroll_to_tab_id = scroll_and_select_tab->ID;
7621 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
7622 tab_bar->SelectedTabId = scroll_to_tab_id;
7623 }
7624
7625 // Shrink widths if full tabs don't fit in their allocated space
7626 float section_0_w = sections[0].Width + sections[0].Spacing;
7627 float section_1_w = sections[1].Width + sections[1].Spacing;
7628 float section_2_w = sections[2].Width + sections[2].Spacing;
7629 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
7630 float width_excess;
7631 if (central_section_is_visible)
7632 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
7633 else
7634 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
7635
7636 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
7637 if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
7638 {
7639 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
7640 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
7641 ShrinkWidths(items: g.ShrinkWidthBuffer.Data + shrink_data_offset, count: shrink_data_count, width_excess);
7642
7643 // Apply shrunk values into tabs and sections
7644 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
7645 {
7646 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
7647 float shrinked_width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width);
7648 if (shrinked_width < 0.0f)
7649 continue;
7650
7651 shrinked_width = ImMax(lhs: 1.0f, rhs: shrinked_width);
7652 int section_n = TabItemGetSectionIdx(tab);
7653 sections[section_n].Width -= (tab->Width - shrinked_width);
7654 tab->Width = shrinked_width;
7655 }
7656 }
7657
7658 // Layout all active tabs
7659 int section_tab_index = 0;
7660 float tab_offset = 0.0f;
7661 tab_bar->WidthAllTabs = 0.0f;
7662 for (int section_n = 0; section_n < 3; section_n++)
7663 {
7664 ImGuiTabBarSection* section = &sections[section_n];
7665 if (section_n == 2)
7666 tab_offset = ImMin(lhs: ImMax(lhs: 0.0f, rhs: tab_bar->BarRect.GetWidth() - section->Width), rhs: tab_offset);
7667
7668 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
7669 {
7670 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
7671 tab->Offset = tab_offset;
7672 tab->NameOffset = -1;
7673 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
7674 }
7675 tab_bar->WidthAllTabs += ImMax(lhs: section->Width + section->Spacing, rhs: 0.0f);
7676 tab_offset += section->Spacing;
7677 section_tab_index += section->TabCount;
7678 }
7679
7680 // Clear name buffers
7681 tab_bar->TabsNames.Buf.resize(new_size: 0);
7682
7683 // If we have lost the selected tab, select the next most recently active one
7684 if (found_selected_tab_id == false)
7685 tab_bar->SelectedTabId = 0;
7686 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
7687 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
7688
7689 // Lock in visible tab
7690 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
7691 tab_bar->VisibleTabWasSubmitted = false;
7692
7693 // CTRL+TAB can override visible tab temporarily
7694 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
7695 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
7696
7697 // Update scrolling
7698 if (scroll_to_tab_id != 0)
7699 TabBarScrollToTab(tab_bar, tab_id: scroll_to_tab_id, sections);
7700 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingAnim);
7701 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, scrolling: tab_bar->ScrollingTarget);
7702 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
7703 {
7704 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
7705 // Teleport if we are aiming far off the visible line
7706 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, rhs: 70.0f * g.FontSize);
7707 tab_bar->ScrollingSpeed = ImMax(lhs: tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
7708 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
7709 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(current: tab_bar->ScrollingAnim, target: tab_bar->ScrollingTarget, speed: g.IO.DeltaTime * tab_bar->ScrollingSpeed);
7710 }
7711 else
7712 {
7713 tab_bar->ScrollingSpeed = 0.0f;
7714 }
7715 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
7716 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
7717
7718 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
7719 ImGuiWindow* window = g.CurrentWindow;
7720 window->DC.CursorPos = tab_bar->BarRect.Min;
7721 ItemSize(size: ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), text_baseline_y: tab_bar->FramePadding.y);
7722 window->DC.IdealMaxPos.x = ImMax(lhs: window->DC.IdealMaxPos.x, rhs: tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
7723}
7724
7725// Dockable uses Name/ID in the global namespace. Non-dockable items use the ID stack.
7726static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
7727{
7728 if (docked_window != NULL)
7729 {
7730 IM_UNUSED(tab_bar);
7731 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
7732 ImGuiID id = docked_window->TabId;
7733 KeepAliveID(id);
7734 return id;
7735 }
7736 else
7737 {
7738 ImGuiWindow* window = GImGui->CurrentWindow;
7739 return window->GetID(str: label);
7740 }
7741}
7742
7743static float ImGui::TabBarCalcMaxTabWidth()
7744{
7745 ImGuiContext& g = *GImGui;
7746 return g.FontSize * 20.0f;
7747}
7748
7749ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
7750{
7751 if (tab_id != 0)
7752 for (int n = 0; n < tab_bar->Tabs.Size; n++)
7753 if (tab_bar->Tabs[n].ID == tab_id)
7754 return &tab_bar->Tabs[n];
7755 return NULL;
7756}
7757
7758// FIXME: See references to #2304 in TODO.txt
7759ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
7760{
7761 ImGuiTabItem* most_recently_selected_tab = NULL;
7762 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
7763 {
7764 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
7765 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
7766 if (tab->Window && tab->Window->WasActive)
7767 most_recently_selected_tab = tab;
7768 }
7769 return most_recently_selected_tab;
7770}
7771
7772// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
7773// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
7774void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
7775{
7776 ImGuiContext& g = *GImGui;
7777 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
7778 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)
7779
7780 if (!window->HasCloseButton)
7781 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
7782
7783 ImGuiTabItem new_tab;
7784 new_tab.ID = window->TabId;
7785 new_tab.Flags = tab_flags;
7786 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
7787 if (new_tab.LastFrameVisible == -1)
7788 new_tab.LastFrameVisible = g.FrameCount - 1;
7789 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
7790 tab_bar->Tabs.push_back(v: new_tab);
7791}
7792
7793// The *TabId fields be already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
7794void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
7795{
7796 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
7797 tab_bar->Tabs.erase(it: tab);
7798 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
7799 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
7800 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
7801}
7802
7803// Called on manual closure attempt
7804void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
7805{
7806 if (tab->Flags & ImGuiTabItemFlags_Button)
7807 return; // A button appended with TabItemButton().
7808
7809 if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument))
7810 {
7811 // This will remove a frame of lag for selecting another tab on closure.
7812 // 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
7813 tab->WantClose = true;
7814 if (tab_bar->VisibleTabId == tab->ID)
7815 {
7816 tab->LastFrameVisible = -1;
7817 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
7818 }
7819 }
7820 else
7821 {
7822 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
7823 if (tab_bar->VisibleTabId != tab->ID)
7824 tab_bar->NextSelectedTabId = tab->ID;
7825 }
7826}
7827
7828static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
7829{
7830 scrolling = ImMin(lhs: scrolling, rhs: tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
7831 return ImMax(lhs: scrolling, rhs: 0.0f);
7832}
7833
7834// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
7835static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
7836{
7837 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
7838 if (tab == NULL)
7839 return;
7840 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
7841 return;
7842
7843 ImGuiContext& g = *GImGui;
7844 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)
7845 int order = tab_bar->GetTabOrder(tab);
7846
7847 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
7848 // FIXME: This is all confusing.
7849 float scrollable_width = tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
7850
7851 // We make all tabs positions all relative Sections[0].Width to make code simpler
7852 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
7853 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
7854 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
7855 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
7856 {
7857 // Scroll to the left
7858 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: tab_bar->ScrollingAnim - tab_x2, rhs: 0.0f);
7859 tab_bar->ScrollingTarget = tab_x1;
7860 }
7861 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
7862 {
7863 // Scroll to the right
7864 tab_bar->ScrollingTargetDistToVisibility = ImMax(lhs: (tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, rhs: 0.0f);
7865 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
7866 }
7867}
7868
7869void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int offset)
7870{
7871 IM_ASSERT(offset != 0);
7872 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
7873 tab_bar->ReorderRequestTabId = tab->ID;
7874 tab_bar->ReorderRequestOffset = (ImS16)offset;
7875}
7876
7877void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, const ImGuiTabItem* src_tab, ImVec2 mouse_pos)
7878{
7879 ImGuiContext& g = *GImGui;
7880 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
7881 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
7882 return;
7883
7884 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
7885 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
7886
7887 // Count number of contiguous tabs we are crossing over
7888 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
7889 const int src_idx = tab_bar->Tabs.index_from_ptr(it: src_tab);
7890 int dst_idx = src_idx;
7891 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
7892 {
7893 // Reordered tabs must share the same section
7894 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
7895 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
7896 break;
7897 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
7898 break;
7899 dst_idx = i;
7900
7901 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
7902 const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
7903 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
7904 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
7905 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
7906 break;
7907 }
7908
7909 if (dst_idx != src_idx)
7910 TabBarQueueReorder(tab_bar, tab: src_tab, offset: dst_idx - src_idx);
7911}
7912
7913bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
7914{
7915 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_id: tab_bar->ReorderRequestTabId);
7916 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
7917 return false;
7918
7919 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
7920 int tab2_order = tab_bar->GetTabOrder(tab: tab1) + tab_bar->ReorderRequestOffset;
7921 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
7922 return false;
7923
7924 // Reordered tabs must share the same section
7925 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
7926 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
7927 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
7928 return false;
7929 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
7930 return false;
7931
7932 ImGuiTabItem item_tmp = *tab1;
7933 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
7934 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
7935 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
7936 memmove(dest: dst_tab, src: src_tab, n: move_count * sizeof(ImGuiTabItem));
7937 *tab2 = item_tmp;
7938
7939 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
7940 MarkIniSettingsDirty();
7941 return true;
7942}
7943
7944static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
7945{
7946 ImGuiContext& g = *GImGui;
7947 ImGuiWindow* window = g.CurrentWindow;
7948
7949 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
7950 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
7951
7952 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
7953 //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));
7954
7955 int select_dir = 0;
7956 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
7957 arrow_col.w *= 0.5f;
7958
7959 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
7960 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
7961 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
7962 const float backup_repeat_rate = g.IO.KeyRepeatRate;
7963 g.IO.KeyRepeatDelay = 0.250f;
7964 g.IO.KeyRepeatRate = 0.200f;
7965 float x = ImMax(lhs: tab_bar->BarRect.Min.x, rhs: tab_bar->BarRect.Max.x - scrolling_buttons_width);
7966 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
7967 if (ArrowButtonEx(str_id: "##<", dir: ImGuiDir_Left, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
7968 select_dir = -1;
7969 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
7970 if (ArrowButtonEx(str_id: "##>", dir: ImGuiDir_Right, size: arrow_button_size, flags: ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat))
7971 select_dir = +1;
7972 PopStyleColor(count: 2);
7973 g.IO.KeyRepeatRate = backup_repeat_rate;
7974 g.IO.KeyRepeatDelay = backup_repeat_delay;
7975
7976 ImGuiTabItem* tab_to_scroll_to = NULL;
7977 if (select_dir != 0)
7978 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_id: tab_bar->SelectedTabId))
7979 {
7980 int selected_order = tab_bar->GetTabOrder(tab: tab_item);
7981 int target_order = selected_order + select_dir;
7982
7983 // Skip tab item buttons until another tab item is found or end is reached
7984 while (tab_to_scroll_to == NULL)
7985 {
7986 // If we are at the end of the list, still scroll to make our tab visible
7987 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
7988
7989 // Cross through buttons
7990 // (even if first/last item is a button, return it so we can update the scroll)
7991 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
7992 {
7993 target_order += select_dir;
7994 selected_order += select_dir;
7995 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
7996 }
7997 }
7998 }
7999 window->DC.CursorPos = backup_cursor_pos;
8000 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
8001
8002 return tab_to_scroll_to;
8003}
8004
8005static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
8006{
8007 ImGuiContext& g = *GImGui;
8008 ImGuiWindow* window = g.CurrentWindow;
8009
8010 // We use g.Style.FramePadding.y to match the square ArrowButton size
8011 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
8012 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
8013 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
8014 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
8015
8016 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
8017 arrow_col.w *= 0.5f;
8018 PushStyleColor(idx: ImGuiCol_Text, col: arrow_col);
8019 PushStyleColor(idx: ImGuiCol_Button, col: ImVec4(0, 0, 0, 0));
8020 bool open = BeginCombo(label: "##v", NULL, flags: ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
8021 PopStyleColor(count: 2);
8022
8023 ImGuiTabItem* tab_to_select = NULL;
8024 if (open)
8025 {
8026 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
8027 {
8028 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
8029 if (tab->Flags & ImGuiTabItemFlags_Button)
8030 continue;
8031
8032 const char* tab_name = tab_bar->GetTabName(tab);
8033 if (Selectable(label: tab_name, selected: tab_bar->SelectedTabId == tab->ID))
8034 tab_to_select = tab;
8035 }
8036 EndCombo();
8037 }
8038
8039 window->DC.CursorPos = backup_cursor_pos;
8040 return tab_to_select;
8041}
8042
8043//-------------------------------------------------------------------------
8044// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
8045//-------------------------------------------------------------------------
8046// - BeginTabItem()
8047// - EndTabItem()
8048// - TabItemButton()
8049// - TabItemEx() [Internal]
8050// - SetTabItemClosed()
8051// - TabItemCalcSize() [Internal]
8052// - TabItemBackground() [Internal]
8053// - TabItemLabelAndCloseButton() [Internal]
8054//-------------------------------------------------------------------------
8055
8056bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
8057{
8058 ImGuiContext& g = *GImGui;
8059 ImGuiWindow* window = g.CurrentWindow;
8060 if (window->SkipItems)
8061 return false;
8062
8063 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8064 if (tab_bar == NULL)
8065 {
8066 IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
8067 return false;
8068 }
8069 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
8070
8071 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
8072 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
8073 {
8074 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
8075 PushOverrideID(id: tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
8076 }
8077 return ret;
8078}
8079
8080void ImGui::EndTabItem()
8081{
8082 ImGuiContext& g = *GImGui;
8083 ImGuiWindow* window = g.CurrentWindow;
8084 if (window->SkipItems)
8085 return;
8086
8087 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8088 if (tab_bar == NULL)
8089 {
8090 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
8091 return;
8092 }
8093 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
8094 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
8095 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
8096 PopID();
8097}
8098
8099bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
8100{
8101 ImGuiContext& g = *GImGui;
8102 ImGuiWindow* window = g.CurrentWindow;
8103 if (window->SkipItems)
8104 return false;
8105
8106 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8107 if (tab_bar == NULL)
8108 {
8109 IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
8110 return false;
8111 }
8112 return TabItemEx(tab_bar, label, NULL, flags: flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
8113}
8114
8115bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
8116{
8117 // Layout whole tab bar if not already done
8118 ImGuiContext& g = *GImGui;
8119 if (tab_bar->WantLayout)
8120 {
8121 ImGuiNextItemData backup_next_item_data = g.NextItemData;
8122 TabBarLayout(tab_bar);
8123 g.NextItemData = backup_next_item_data;
8124 }
8125 ImGuiWindow* window = g.CurrentWindow;
8126 if (window->SkipItems)
8127 return false;
8128
8129 const ImGuiStyle& style = g.Style;
8130 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
8131
8132 // If the user called us with *p_open == false, we early out and don't render.
8133 // 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.
8134 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
8135 if (p_open && !*p_open)
8136 {
8137 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
8138 return false;
8139 }
8140
8141 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
8142 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
8143
8144 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
8145 if (flags & ImGuiTabItemFlags_NoCloseButton)
8146 p_open = NULL;
8147 else if (p_open == NULL)
8148 flags |= ImGuiTabItemFlags_NoCloseButton;
8149
8150 // Acquire tab data
8151 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id: id);
8152 bool tab_is_new = false;
8153 if (tab == NULL)
8154 {
8155 tab_bar->Tabs.push_back(v: ImGuiTabItem());
8156 tab = &tab_bar->Tabs.back();
8157 tab->ID = id;
8158 tab_bar->TabsAddedNew = tab_is_new = true;
8159 }
8160 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(it: tab);
8161
8162 // Calculate tab contents size
8163 ImVec2 size = TabItemCalcSize(label, has_close_button_or_unsaved_marker: (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
8164 tab->RequestedWidth = -1.0f;
8165 if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth)
8166 size.x = tab->RequestedWidth = g.NextItemData.Width;
8167 if (tab_is_new)
8168 tab->Width = ImMax(lhs: 1.0f, rhs: size.x);
8169 tab->ContentWidth = size.x;
8170 tab->BeginOrder = tab_bar->TabsActiveCount++;
8171
8172 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
8173 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
8174 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
8175 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
8176 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
8177 tab->LastFrameVisible = g.FrameCount;
8178 tab->Flags = flags;
8179 tab->Window = docked_window;
8180
8181 // Append name with zero-terminator
8182 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
8183 if (tab->Window != NULL)
8184 {
8185 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
8186 tab->NameOffset = -1;
8187 }
8188 else
8189 {
8190 IM_ASSERT(tab->Window == NULL);
8191 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
8192 tab_bar->TabsNames.append(str: label, str_end: label + strlen(s: label) + 1); // Append name _with_ the zero-terminator.
8193 }
8194
8195 // Update selected tab
8196 if (!is_tab_button)
8197 {
8198 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
8199 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
8200 tab_bar->NextSelectedTabId = id; // New tabs gets activated
8201 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
8202 tab_bar->NextSelectedTabId = id;
8203 }
8204
8205 // Lock visibility
8206 // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
8207 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
8208 if (tab_contents_visible)
8209 tab_bar->VisibleTabWasSubmitted = true;
8210
8211 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
8212 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
8213 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
8214 tab_contents_visible = true;
8215
8216 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
8217 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
8218 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
8219 {
8220 ItemAdd(bb: ImRect(), id, NULL, extra_flags: ImGuiItemFlags_NoNav);
8221 if (is_tab_button)
8222 return false;
8223 return tab_contents_visible;
8224 }
8225
8226 if (tab_bar->SelectedTabId == id)
8227 tab->LastFrameSelected = g.FrameCount;
8228
8229 // Backup current layout position
8230 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
8231
8232 // Layout
8233 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
8234 size.x = tab->Width;
8235 if (is_central_section)
8236 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
8237 else
8238 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
8239 ImVec2 pos = window->DC.CursorPos;
8240 ImRect bb(pos, pos + size);
8241
8242 // 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)
8243 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
8244 if (want_clip_rect)
8245 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);
8246
8247 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
8248 ItemSize(size: bb.GetSize(), text_baseline_y: style.FramePadding.y);
8249 window->DC.CursorMaxPos = backup_cursor_max_pos;
8250
8251 if (!ItemAdd(bb, id))
8252 {
8253 if (want_clip_rect)
8254 PopClipRect();
8255 window->DC.CursorPos = backup_main_cursor_pos;
8256 return tab_contents_visible;
8257 }
8258
8259 // Click to Select a tab
8260 ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowItemOverlap);
8261 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
8262 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
8263 bool hovered, held;
8264 bool pressed = ButtonBehavior(bb, id, out_hovered: &hovered, out_held: &held, flags: button_flags);
8265 if (pressed && !is_tab_button)
8266 tab_bar->NextSelectedTabId = id;
8267
8268 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
8269 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
8270 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
8271 g.ActiveIdWindow = docked_window;
8272
8273 // Allow the close button to overlap unless we are dragging (in which case we don't want any overlapping tabs to be hovered)
8274 if (g.ActiveId != id)
8275 SetItemAllowOverlap();
8276
8277 // Drag and drop a single floating window node moves it
8278 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
8279 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
8280 if (held && single_floating_window_node && IsMouseDragging(button: 0, lock_threshold: 0.0f))
8281 {
8282 // Move
8283 StartMouseMovingWindow(window: docked_window);
8284 }
8285 else if (held && !tab_appearing && IsMouseDragging(button: 0))
8286 {
8287 // Drag and drop: re-order tabs
8288 int drag_dir = 0;
8289 float drag_distance_from_edge_x = 0.0f;
8290 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
8291 {
8292 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
8293 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
8294 {
8295 drag_dir = -1;
8296 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
8297 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
8298 }
8299 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
8300 {
8301 drag_dir = +1;
8302 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
8303 TabBarQueueReorderFromMousePos(tab_bar, src_tab: tab, mouse_pos: g.IO.MousePos);
8304 }
8305 }
8306
8307 // Extract a Dockable window out of it's tab bar
8308 if (docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove))
8309 {
8310 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
8311 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
8312 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
8313 {
8314 float threshold_base = g.FontSize;
8315 float threshold_x = (threshold_base * 2.2f);
8316 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);
8317 //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]
8318
8319 float distance_from_edge_y = ImMax(lhs: bb.Min.y - g.IO.MousePos.y, rhs: g.IO.MousePos.y - bb.Max.y);
8320 if (distance_from_edge_y >= threshold_y)
8321 undocking_tab = true;
8322 if (drag_distance_from_edge_x > threshold_x)
8323 if ((drag_dir < 0 && tab_bar->GetTabOrder(tab) == 0) || (drag_dir > 0 && tab_bar->GetTabOrder(tab) == tab_bar->Tabs.Size - 1))
8324 undocking_tab = true;
8325 }
8326
8327 if (undocking_tab)
8328 {
8329 // Undock
8330 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
8331 DockContextQueueUndockWindow(ctx: &g, window: docked_window);
8332 g.MovingWindow = docked_window;
8333 SetActiveID(id: g.MovingWindow->MoveId, window: g.MovingWindow);
8334 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
8335 g.ActiveIdNoClearOnFocusLoss = true;
8336 SetActiveIdUsingAllKeyboardKeys();
8337 }
8338 }
8339 }
8340
8341#if 0
8342 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
8343 {
8344 // Enlarge tab display when hovering
8345 bb.Max.x = bb.Min.x + IM_FLOOR(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
8346 display_draw_list = GetForegroundDrawList(window);
8347 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
8348 }
8349#endif
8350
8351 // Render tab shape
8352 ImDrawList* display_draw_list = window->DrawList;
8353 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));
8354 TabItemBackground(draw_list: display_draw_list, bb, flags, col: tab_col);
8355 RenderNavHighlight(bb, id);
8356
8357 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
8358 const bool hovered_unblocked = IsItemHovered(flags: ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8359 if (hovered_unblocked && (IsMouseClicked(button: 1) || IsMouseReleased(button: 1)))
8360 if (!is_tab_button)
8361 tab_bar->NextSelectedTabId = id;
8362
8363 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
8364 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
8365
8366 // Render tab label, process close button
8367 const ImGuiID close_button_id = p_open ? GetIDWithSeed(str_id_begin: "#CLOSE", NULL, seed: docked_window ? docked_window->ID : id) : 0;
8368 bool just_closed;
8369 bool text_clipped;
8370 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);
8371 if (just_closed && p_open != NULL)
8372 {
8373 *p_open = false;
8374 TabBarCloseTab(tab_bar, tab);
8375 }
8376
8377 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
8378 // That state is copied to window->DockTabItemStatusFlags by our caller.
8379 if (docked_window && (hovered || g.HoveredId == close_button_id))
8380 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
8381
8382 // Restore main window position so user can draw there
8383 if (want_clip_rect)
8384 PopClipRect();
8385 window->DC.CursorPos = backup_main_cursor_pos;
8386
8387 // Tooltip
8388 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
8389 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
8390 // FIXME: This is a mess.
8391 // FIXME: We may want disabled tab to still display the tooltip?
8392 if (text_clipped && g.HoveredId == id && !held)
8393 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
8394 if (IsItemHovered(flags: ImGuiHoveredFlags_DelayNormal))
8395 SetTooltip("%.*s", (int)(FindRenderedTextEnd(text: label) - label), label);
8396
8397 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
8398 if (is_tab_button)
8399 return pressed;
8400 return tab_contents_visible;
8401}
8402
8403// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
8404// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
8405// Tabs closed by the close button will automatically be flagged to avoid this issue.
8406void ImGui::SetTabItemClosed(const char* label)
8407{
8408 ImGuiContext& g = *GImGui;
8409 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
8410 if (is_within_manual_tab_bar)
8411 {
8412 ImGuiTabBar* tab_bar = g.CurrentTabBar;
8413 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
8414 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
8415 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
8416 }
8417 else if (ImGuiWindow* window = FindWindowByName(name: label))
8418 {
8419 if (window->DockIsActive)
8420 if (ImGuiDockNode* node = window->DockNode)
8421 {
8422 ImGuiID tab_id = TabBarCalcTabID(tab_bar: node->TabBar, label, docked_window: window);
8423 TabBarRemoveTab(tab_bar: node->TabBar, tab_id);
8424 window->DockTabWantClose = true;
8425 }
8426 }
8427}
8428
8429ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
8430{
8431 ImGuiContext& g = *GImGui;
8432 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8433 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
8434 if (has_close_button_or_unsaved_marker)
8435 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
8436 else
8437 size.x += g.Style.FramePadding.x + 1.0f;
8438 return ImVec2(ImMin(lhs: size.x, rhs: TabBarCalcMaxTabWidth()), size.y);
8439}
8440
8441ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
8442{
8443 return TabItemCalcSize(label: window->Name, has_close_button_or_unsaved_marker: window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
8444}
8445
8446void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
8447{
8448 // 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.
8449 ImGuiContext& g = *GImGui;
8450 const float width = bb.GetWidth();
8451 IM_UNUSED(flags);
8452 IM_ASSERT(width > 0.0f);
8453 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));
8454 const float y1 = bb.Min.y + 1.0f;
8455 const float y2 = bb.Max.y + ((flags & ImGuiTabItemFlags_Preview) ? 0.0f : -1.0f);
8456 draw_list->PathLineTo(pos: ImVec2(bb.Min.x, y2));
8457 draw_list->PathArcToFast(center: ImVec2(bb.Min.x + rounding, y1 + rounding), radius: rounding, a_min_of_12: 6, a_max_of_12: 9);
8458 draw_list->PathArcToFast(center: ImVec2(bb.Max.x - rounding, y1 + rounding), radius: rounding, a_min_of_12: 9, a_max_of_12: 12);
8459 draw_list->PathLineTo(pos: ImVec2(bb.Max.x, y2));
8460 draw_list->PathFillConvex(col);
8461 if (g.Style.TabBorderSize > 0.0f)
8462 {
8463 draw_list->PathLineTo(pos: ImVec2(bb.Min.x + 0.5f, y2));
8464 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);
8465 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);
8466 draw_list->PathLineTo(pos: ImVec2(bb.Max.x - 0.5f, y2));
8467 draw_list->PathStroke(col: GetColorU32(idx: ImGuiCol_Border), flags: 0, thickness: g.Style.TabBorderSize);
8468 }
8469}
8470
8471// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
8472// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
8473void 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)
8474{
8475 ImGuiContext& g = *GImGui;
8476 ImVec2 label_size = CalcTextSize(text: label, NULL, hide_text_after_double_hash: true);
8477
8478 if (out_just_closed)
8479 *out_just_closed = false;
8480 if (out_text_clipped)
8481 *out_text_clipped = false;
8482
8483 if (bb.GetWidth() <= 1.0f)
8484 return;
8485
8486 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
8487 // But right now if you want to alter text color of tabs this is what you need to do.
8488#if 0
8489 const float backup_alpha = g.Style.Alpha;
8490 if (!is_contents_visible)
8491 g.Style.Alpha *= 0.7f;
8492#endif
8493
8494 // Render text label (with clipping + alpha gradient) + unsaved marker
8495 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);
8496 ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
8497
8498 // Return clipped state ignoring the close button
8499 if (out_text_clipped)
8500 {
8501 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
8502 //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));
8503 }
8504
8505 const float button_sz = g.FontSize;
8506 const ImVec2 button_pos(ImMax(lhs: bb.Min.x, rhs: bb.Max.x - frame_padding.x * 2.0f - button_sz), bb.Min.y);
8507
8508 // Close Button & Unsaved Marker
8509 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
8510 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
8511 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
8512 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
8513 bool close_button_pressed = false;
8514 bool close_button_visible = false;
8515 if (close_button_id != 0)
8516 if (is_contents_visible || bb.GetWidth() >= ImMax(lhs: button_sz, rhs: g.Style.TabMinWidthForCloseButton))
8517 if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
8518 close_button_visible = true;
8519 bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
8520
8521 if (close_button_visible)
8522 {
8523 ImGuiLastItemData last_item_backup = g.LastItemData;
8524 PushStyleVar(idx: ImGuiStyleVar_FramePadding, val: frame_padding);
8525 if (CloseButton(id: close_button_id, pos: button_pos))
8526 close_button_pressed = true;
8527 PopStyleVar();
8528 g.LastItemData = last_item_backup;
8529
8530 // Close with middle mouse button
8531 if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(button: 2))
8532 close_button_pressed = true;
8533 }
8534 else if (unsaved_marker_visible)
8535 {
8536 const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz) + g.Style.FramePadding * 2.0f);
8537 RenderBullet(draw_list, pos: bullet_bb.GetCenter(), col: GetColorU32(idx: ImGuiCol_Text));
8538 }
8539
8540 // This is all rather complicated
8541 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
8542 // 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..
8543 float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
8544 if (close_button_visible || unsaved_marker_visible)
8545 {
8546 text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
8547 text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
8548 ellipsis_max_x = text_pixel_clip_bb.Max.x;
8549 }
8550 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);
8551
8552#if 0
8553 if (!is_contents_visible)
8554 g.Style.Alpha = backup_alpha;
8555#endif
8556
8557 if (out_just_closed)
8558 *out_just_closed = close_button_pressed;
8559}
8560
8561
8562#endif // #ifndef IMGUI_DISABLE
8563

Provided by KDAB

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

source code of flutter_engine/third_party/imgui/imgui_widgets.cpp