546.6 KB、11123行 生 Blame 履歴
1// dear imgui, v1.92.9 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: Typing-Select support
22// [SECTION] Widgets: Box-Select support
23// [SECTION] Widgets: Multi-Select support
24// [SECTION] Widgets: Multi-Select helpers
25// [SECTION] Widgets: ListBox
26// [SECTION] Widgets: PlotLines, PlotHistogram
27// [SECTION] Widgets: Value helpers
28// [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc.
29// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
30// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
31// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc.
32
33*/
34
35#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
36#define _CRT_SECURE_NO_WARNINGS
37#endif
38
39#ifndef IMGUI_DEFINE_MATH_OPERATORS
40#define IMGUI_DEFINE_MATH_OPERATORS
41#endif
42
43#include "imgui.h"
44#ifndef IMGUI_DISABLE
45#include "imgui_internal.h"
46
47// System includes
48#include <stdint.h> // intptr_t
49
50//-------------------------------------------------------------------------
51// Warnings
52//-------------------------------------------------------------------------
53
54// Visual Studio warnings
55#ifdef _MSC_VER
56#pragma warning (disable: 4127) // condition expression is constant
57#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen
58#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later
59#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types
60#endif
61#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2).
62#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
63#endif
64
65// Clang/GCC warnings with -Weverything
66#if defined(__clang__)
67#if __has_warning("-Wunknown-warning-option")
68#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great!
69#endif
70#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx'
71#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse.
72#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok.
73#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int'
74#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code.
75#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
76#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used.
77#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
78#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double.
79#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
80#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
81#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
82#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access
83#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type
84#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label
85#elif defined(__GNUC__)
86#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
87#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe
88#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*'
89#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
90#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
91#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function
92#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1
93#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead
94#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers
95#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may change value
96#pragma GCC diagnostic ignored "-Wsign-conversion" // warning: conversion to 'xxxx' from 'xxxx' may change the sign of the result
97#endif
98
99//-------------------------------------------------------------------------
100// Data
101//-------------------------------------------------------------------------
102
103// Widgets
104static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
105static 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.
106
107// Those MIN/MAX values are not define because we need to point to them
108static const signed char IM_S8_MIN = -128;
109static const signed char IM_S8_MAX = 127;
110static const unsigned char IM_U8_MIN = 0;
111static const unsigned char IM_U8_MAX = 0xFF;
112static const signed short IM_S16_MIN = -32768;
113static const signed short IM_S16_MAX = 32767;
114static const unsigned short IM_U16_MIN = 0;
115static const unsigned short IM_U16_MAX = 0xFFFF;
116static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
117static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
118static const ImU32 IM_U32_MIN = 0;
119static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
120#ifdef LLONG_MIN
121static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
122static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
123#else
124static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
125static const ImS64 IM_S64_MAX = 9223372036854775807LL;
126#endif
127static const ImU64 IM_U64_MIN = 0;
128#ifdef ULLONG_MAX
129static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
130#else
131static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
132#endif
133
134//-------------------------------------------------------------------------
135// [SECTION] Forward Declarations
136//-------------------------------------------------------------------------
137
138// For InputTextEx()
139static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* state, unsigned int* p_char, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
140static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
141
142//-------------------------------------------------------------------------
143// [SECTION] Widgets: Text, etc.
144//-------------------------------------------------------------------------
145// - TextEx() [Internal]
146// - TextUnformatted()
147// - Text()
148// - TextV()
149// - TextColored()
150// - TextColoredV()
151// - TextDisabled()
152// - TextDisabledV()
153// - TextWrapped()
154// - TextWrappedV()
155// - LabelText()
156// - LabelTextV()
157// - BulletText()
158// - BulletTextV()
159//-------------------------------------------------------------------------
160
161void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
162{
163 ImGuiWindow* window = GetCurrentWindow();
164 if (window->SkipItems)
165 return;
166 ImGuiContext& g = *GImGui;
167
168 // Accept null ranges
169 if (text == text_end)
170 text = text_end = "";
171
172 // Calculate length
173 const char* text_begin = text;
174 if (text_end == NULL)
175 text_end = text + ImStrlen(text); // FIXME-OPT
176
177 const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
178 const float wrap_pos_x = window->DC.TextWrapPos;
179 const bool wrap_enabled = (wrap_pos_x >= 0.0f);
180 if (text_end - text <= 2000 || wrap_enabled)
181 {
182 // Common case
183 const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
184 const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
185
186 ImRect bb(text_pos, text_pos + text_size);
187 ItemSize(text_size, 0.0f);
188 if (!ItemAdd(bb, 0))
189 return;
190
191 // Render (we don't hide text after ## in this end-user function)
192 RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
193 }
194 else
195 {
196 // Long text!
197 // Perform manual coarse clipping to optimize for long multi-line text
198 // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
199 // - 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.
200 // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
201 const char* line = text;
202 const float line_height = GetTextLineHeight();
203 ImVec2 text_size(0, 0);
204
205 // Lines to skip (can't skip when logging text)
206 ImVec2 pos = text_pos;
207 if (!g.LogEnabled)
208 {
209 int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
210 if (lines_skippable > 0)
211 {
212 int lines_skipped = 0;
213 while (line < text_end && lines_skipped < lines_skippable)
214 {
215 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
216 if (!line_end)
217 line_end = text_end;
218 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
219 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
220 line = line_end + 1;
221 lines_skipped++;
222 }
223 pos.y += lines_skipped * line_height;
224 }
225 }
226
227 // Lines to render
228 if (line < text_end)
229 {
230 ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
231 while (line < text_end)
232 {
233 if (IsClippedEx(line_rect, 0))
234 break;
235
236 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
237 if (!line_end)
238 line_end = text_end;
239 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
240 RenderText(pos, line, line_end, false);
241 line = line_end + 1;
242 line_rect.Min.y += line_height;
243 line_rect.Max.y += line_height;
244 pos.y += line_height;
245 }
246
247 // Count remaining lines
248 int lines_skipped = 0;
249 while (line < text_end)
250 {
251 const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
252 if (!line_end)
253 line_end = text_end;
254 if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
255 text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
256 line = line_end + 1;
257 lines_skipped++;
258 }
259 pos.y += lines_skipped * line_height;
260 }
261 text_size.y = (pos - text_pos).y;
262
263 ImRect bb(text_pos, text_pos + text_size);
264 ItemSize(text_size, 0.0f);
265 ItemAdd(bb, 0);
266 }
267}
268
269// Note that all functions taking format strings in the API may be passed ("%s", text) or ("%.*s", text_len, text),
270// which will automatically bypass the formatter.
271void ImGui::TextUnformatted(const char* text, const char* text_end)
272{
273 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
274}
275
276void ImGui::Text(const char* fmt, ...)
277{
278 va_list args;
279 va_start(args, fmt);
280 TextV(fmt, args);
281 va_end(args);
282}
283
284void ImGui::TextV(const char* fmt, va_list args)
285{
286 ImGuiWindow* window = GetCurrentWindow();
287 if (window->SkipItems)
288 return;
289
290 const char* text, *text_end;
291 ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
292 TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
293}
294
295void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
296{
297 va_list args;
298 va_start(args, fmt);
299 TextColoredV(col, fmt, args);
300 va_end(args);
301}
302
303void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
304{
305 PushStyleColor(ImGuiCol_Text, col);
306 TextV(fmt, args);
307 PopStyleColor();
308}
309
310void ImGui::TextDisabled(const char* fmt, ...)
311{
312 va_list args;
313 va_start(args, fmt);
314 TextDisabledV(fmt, args);
315 va_end(args);
316}
317
318void ImGui::TextDisabledV(const char* fmt, va_list args)
319{
320 ImGuiContext& g = *GImGui;
321 PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
322 TextV(fmt, args);
323 PopStyleColor();
324}
325
326void ImGui::TextWrapped(const char* fmt, ...)
327{
328 va_list args;
329 va_start(args, fmt);
330 TextWrappedV(fmt, args);
331 va_end(args);
332}
333
334void ImGui::TextWrappedV(const char* fmt, va_list args)
335{
336 ImGuiContext& g = *GImGui;
337 const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
338 if (need_backup)
339 PushTextWrapPos(0.0f);
340 TextV(fmt, args);
341 if (need_backup)
342 PopTextWrapPos();
343}
344
345void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
346{
347 va_list args;
348 va_start(args, fmt);
349 TextAlignedV(align_x, size_x, fmt, args);
350 va_end(args);
351}
352
353// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
354// size_x : 0.0f = shortcut for GetContentRegionAvail().x
355// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
356void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
357{
358 ImGuiWindow* window = GetCurrentWindow();
359 if (window->SkipItems)
360 return;
361
362 const char* text, *text_end;
363 ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
364 const ImVec2 text_size = CalcTextSize(text, text_end);
365 size_x = CalcItemSize(ImVec2(size_x, 0.0f), 0.0f, text_size.y).x;
366
367 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
368 ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
369 ImVec2 size(ImMin(size_x, text_size.x), text_size.y);
370 window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, pos.x + text_size.x);
371 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, pos.x + text_size.x);
372 if (align_x > 0.0f && text_size.x < size_x)
373 pos.x += ImTrunc((size_x - text_size.x) * align_x);
374 RenderTextEllipsis(window->DrawList, pos, pos_max, pos_max.x, text, text_end, &text_size);
375
376 const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
377 ItemSize(size);
378 ItemAdd(ImRect(pos, pos + size), 0);
379 window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
380
381 if (size_x < text_size.x && IsItemHovered(ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
382 SetTooltip("%.*s", (int)(text_end - text), text);
383}
384
385void ImGui::LabelText(const char* label, const char* fmt, ...)
386{
387 va_list args;
388 va_start(args, fmt);
389 LabelTextV(label, fmt, args);
390 va_end(args);
391}
392
393// Add a label+text combo aligned to other label+value widgets
394void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
395{
396 ImGuiWindow* window = GetCurrentWindow();
397 if (window->SkipItems)
398 return;
399
400 ImGuiContext& g = *GImGui;
401 const ImGuiStyle& style = g.Style;
402 const float w = CalcItemWidth();
403
404 const char* value_text_begin, *value_text_end;
405 ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
406 const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
407 const char* label_end = FindRenderedTextEnd(label);
408 const ImVec2 label_size = CalcTextSize(label, label_end, false);
409
410 const ImVec2 pos = window->DC.CursorPos;
411 const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
412 const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2));
413 ItemSize(total_bb, style.FramePadding.y);
414 if (!ItemAdd(total_bb, 0))
415 return;
416
417 // Render
418 RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
419 if (label_size.x > 0.0f)
420 RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label, label_end, false);
421}
422
423void ImGui::BulletText(const char* fmt, ...)
424{
425 va_list args;
426 va_start(args, fmt);
427 BulletTextV(fmt, args);
428 va_end(args);
429}
430
431// Text with a little bullet aligned to the typical tree node.
432void ImGui::BulletTextV(const char* fmt, va_list args)
433{
434 ImGuiWindow* window = GetCurrentWindow();
435 if (window->SkipItems)
436 return;
437
438 ImGuiContext& g = *GImGui;
439 const ImGuiStyle& style = g.Style;
440
441 const char* text_begin, *text_end;
442 ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
443 const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
444 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
445 ImVec2 pos = window->DC.CursorPos;
446 pos.y += window->DC.CurrLineTextBaseOffset;
447 ItemSize(total_size, 0.0f);
448 const ImRect bb(pos, pos + total_size);
449 if (!ItemAdd(bb, 0))
450 return;
451
452 // Render
453 ImU32 text_col = GetColorU32(ImGuiCol_Text);
454 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
455 RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
456}
457
458//-------------------------------------------------------------------------
459// [SECTION] Widgets: Main
460//-------------------------------------------------------------------------
461// - ButtonBehavior() [Internal]
462// - Button()
463// - SmallButton()
464// - InvisibleButton()
465// - ArrowButton()
466// - CloseButton() [Internal]
467// - CollapseButton() [Internal]
468// - GetWindowScrollbarID() [Internal]
469// - GetWindowScrollbarRect() [Internal]
470// - Scrollbar() [Internal]
471// - ScrollbarEx() [Internal]
472// - Image()
473// - ImageButton()
474// - Checkbox()
475// - CheckboxFlagsT() [Internal]
476// - CheckboxFlags()
477// - RadioButton()
478// - ProgressBar()
479// - Bullet()
480// - Hyperlink()
481//-------------------------------------------------------------------------
482
483// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
484// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
485// this code is a little complex.
486// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
487// See the series of events below and the corresponding state reported by dear imgui:
488//------------------------------------------------------------------------------------------------------------------------------------------------
489// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
490// Frame N+0 (mouse is outside bb) - - - - - -
491// Frame N+1 (mouse moves inside bb) - true - - - -
492// Frame N+2 (mouse button is down) - true true true - true
493// Frame N+3 (mouse button is down) - true true - - -
494// Frame N+4 (mouse moves outside bb) - - true - - -
495// Frame N+5 (mouse moves inside bb) - true true - - -
496// Frame N+6 (mouse button is released) true true - - true -
497// Frame N+7 (mouse button is released) - true - - - -
498// Frame N+8 (mouse moves outside bb) - - - - - -
499//------------------------------------------------------------------------------------------------------------------------------------------------
500// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
501// Frame N+2 (mouse button is down) true true true true - true
502// Frame N+3 (mouse button is down) - true true - - -
503// Frame N+6 (mouse button is released) - true - - true -
504// Frame N+7 (mouse button is released) - true - - - -
505//------------------------------------------------------------------------------------------------------------------------------------------------
506// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
507// Frame N+2 (mouse button is down) - true - - - true
508// Frame N+3 (mouse button is down) - true - - - -
509// Frame N+6 (mouse button is released) true true - - - -
510// Frame N+7 (mouse button is released) - true - - - -
511//------------------------------------------------------------------------------------------------------------------------------------------------
512// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
513// Frame N+0 (mouse button is down) - true - - - true
514// Frame N+1 (mouse button is down) - true - - - -
515// Frame N+2 (mouse button is released) - true - - - -
516// Frame N+3 (mouse button is released) - true - - - -
517// Frame N+4 (mouse button is down) true true true true - true
518// Frame N+5 (mouse button is down) - true true - - -
519// Frame N+6 (mouse button is released) - true - - true -
520// Frame N+7 (mouse button is released) - true - - - -
521//------------------------------------------------------------------------------------------------------------------------------------------------
522// Note that some combinations are supported,
523// - PressedOnDragDropHold can generally be associated with any flag.
524// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
525//------------------------------------------------------------------------------------------------------------------------------------------------
526// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
527// Repeat+ Repeat+ Repeat+ Repeat+
528// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
529//-------------------------------------------------------------------------------------------------------------------------------------------------
530// Frame N+0 (mouse button is down) - true - true
531// ... - - - -
532// Frame N + RepeatDelay true true - true
533// ... - - - -
534// Frame N + RepeatDelay + RepeatRate*N true true - true
535//-------------------------------------------------------------------------------------------------------------------------------------------------
536
537// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
538// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
539// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
540// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
541// One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
542// with same ID and different MouseButton (see #8030). You can fix it by:
543// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
544// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
545bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
546{
547 ImGuiContext& g = *GImGui;
548 ImGuiWindow* window = GetCurrentWindow();
549
550 // Default behavior inherited from item flags
551 // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
552 ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
553 if (flags & ImGuiButtonFlags_AllowOverlap)
554 item_flags |= ImGuiItemFlags_AllowOverlap;
555 if (item_flags & ImGuiItemFlags_NoFocus)
556 flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
557
558 // Default only reacts to left mouse button
559 if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
560 flags |= ImGuiButtonFlags_MouseButtonLeft;
561
562 // Default behavior requires click + release inside bounding box
563 if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
564 flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
565
566 ImGuiWindow* backup_hovered_window = g.HoveredWindow;
567 const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
568 if (flatten_hovered_children)
569 g.HoveredWindow = window;
570
571#ifdef IMGUI_ENABLE_TEST_ENGINE
572 // Alternate registration spot, for when caller didn't use ItemAdd()
573 if (g.LastItemData.ID != id)
574 IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
575#endif
576
577 bool pressed = false;
578 bool hovered = ItemHoverable(bb, id, item_flags);
579
580 // Special mode for Drag and Drop used by openables (tree nodes, tabs etc.)
581 // where holding the button pressed for a long time while drag a payload item triggers the button.
582 if (g.DragDropActive)
583 {
584 if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
585 {
586 hovered = true;
587 SetHoveredID(id);
588 if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
589 {
590 pressed = true;
591 g.DragDropHoldJustPressedId = id;
592 FocusWindow(window);
593 }
594 }
595 if (g.DragDropAcceptIdPrev == id && (g.DragDropAcceptFlagsPrev & ImGuiDragDropFlags_AcceptDrawAsHovered))
596 hovered = true;
597 }
598
599 if (flatten_hovered_children)
600 g.HoveredWindow = backup_hovered_window;
601
602 // Mouse handling
603 const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
604 if (hovered)
605 {
606 IM_ASSERT(id != 0); // Lazily check inside rare path.
607
608 // Poll mouse buttons
609 // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
610 // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
611 int mouse_button_clicked = -1;
612 int mouse_button_released = -1;
613 for (int button = 0; button < 3; button++)
614 if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
615 {
616 if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
617 if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
618 }
619
620 // Process initial action
621 const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
622 if (mods_ok)
623 {
624 if (mouse_button_clicked != -1 && g.ActiveId != id)
625 {
626 if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
627 SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
628 if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
629 {
630 SetActiveID(id, window);
631 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
632 if (!(flags & ImGuiButtonFlags_NoNavFocus))
633 {
634 SetFocusID(id, window);
635 FocusWindow(window);
636 }
637 else if (!(flags & ImGuiButtonFlags_NoFocus))
638 {
639 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
640 }
641 }
642 if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
643 {
644 pressed = true;
645 if (flags & ImGuiButtonFlags_NoHoldingActiveId)
646 ClearActiveID();
647 else
648 SetActiveID(id, window); // Hold on ID
649 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
650 if (!(flags & ImGuiButtonFlags_NoNavFocus))
651 {
652 SetFocusID(id, window);
653 FocusWindow(window);
654 }
655 else if (!(flags & ImGuiButtonFlags_NoFocus))
656 {
657 FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
658 }
659 }
660 if (flags & ImGuiButtonFlags_PressedOnRelease)
661 {
662 // FIXME: Traditionally ImGuiButtonFlags_PressedOnRelease never took ActiveId. Adding it in 2026-03-20 since ImGuiButtonFlags_NoHoldingActiveId can always be added.
663 // We don't yet perform an explicit ClearActiveID() to reduce scope of change, but this possibility could be investigated.
664 if (!(flags & ImGuiButtonFlags_NoHoldingActiveId))
665 SetActiveID(id, window); // Hold on ID
666 g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
667 }
668 }
669 if (flags & ImGuiButtonFlags_PressedOnRelease)
670 {
671 if (mouse_button_released != -1)
672 {
673 const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior
674 if (!has_repeated_at_least_once)
675 pressed = true;
676 if (!(flags & ImGuiButtonFlags_NoNavFocus))
677 SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
678 ClearActiveID();
679 }
680 }
681
682 // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
683 // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
684 if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
685 if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
686 pressed = true;
687 }
688
689 if (pressed && g.IO.ConfigNavCursorVisibleAuto)
690 g.NavCursorVisible = false;
691 }
692
693 // Keyboard/Gamepad navigation handling
694 // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
695 if ((item_flags & ImGuiItemFlags_Disabled) == 0)
696 {
697 if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
698 if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
699 hovered = true;
700 if (g.NavActivateDownId == id)
701 {
702 bool nav_activated_by_code = (g.NavActivateId == id);
703 bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
704 if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
705 {
706 // Avoid pressing multiple keys from triggering excessive amount of repeat events
707 const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
708 const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
709 const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
710 const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
711 nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
712 }
713 if (nav_activated_by_code || nav_activated_by_inputs)
714 {
715 // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
716 pressed = true;
717 SetActiveID(id, window);
718 g.ActiveIdSource = g.NavInputSource;
719 if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
720 SetFocusID(id, window);
721 if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
722 g.ActiveIdFromShortcut = true;
723 }
724 }
725 }
726
727 // Process while held
728 bool held = false;
729 if (g.ActiveId == id)
730 {
731 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
732 {
733 if (g.ActiveIdIsJustActivated)
734 g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
735
736 const int mouse_button = g.ActiveIdMouseButton;
737 if (mouse_button == -1)
738 {
739 // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
740 ClearActiveID();
741 }
742 else if (IsMouseDown(mouse_button, test_owner_id))
743 {
744 held = true;
745 }
746 else
747 {
748 bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
749 bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
750 if ((release_in || release_anywhere) && !g.DragDropActive)
751 {
752 // Report as pressed when releasing the mouse (this is the most common path)
753 bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
754 bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
755 bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
756 if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
757 pressed = true;
758 }
759 ClearActiveID();
760 }
761 if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
762 g.NavCursorVisible = false;
763 }
764 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
765 {
766 // When activated using Nav, we hold on the ActiveID until activation button is released
767 if (g.NavActivateDownId == id)
768 held = true; // hovered == true not true as we are already likely hovered on direct activation.
769 else
770 ClearActiveID();
771 }
772 if (pressed)
773 g.ActiveIdHasBeenPressedBefore = true;
774 }
775
776 // Activation highlight (this may be a remote activation)
777 if (g.NavHighlightActivatedId == id && (item_flags & ImGuiItemFlags_Disabled) == 0)
778 hovered = true;
779
780 if (out_hovered) *out_hovered = hovered;
781 if (out_held) *out_held = held;
782
783 return pressed;
784}
785
786bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
787{
788 ImGuiWindow* window = GetCurrentWindow();
789 if (window->SkipItems)
790 return false;
791
792 ImGuiContext& g = *GImGui;
793 const ImGuiStyle& style = g.Style;
794 const ImGuiID id = window->GetID(label);
795 const char* label_end = FindRenderedTextEnd(label);
796 const ImVec2 label_size = CalcTextSize(label, label_end, false);
797
798 ImVec2 pos = window->DC.CursorPos;
799 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)
800 pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
801 ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
802
803 const ImRect bb(pos, pos + size);
804 ItemSize(size, style.FramePadding.y);
805 if (!ItemAdd(bb, id))
806 return false;
807
808 bool hovered, held;
809 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
810
811 // Render
812 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
813 RenderNavCursor(bb, id);
814 RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
815
816 if (g.LogEnabled)
817 LogSetNextTextDecoration("[", "]");
818 RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, label_end, &label_size, style.ButtonTextAlign, &bb);
819
820 // Automatically close popups
821 //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
822 // CloseCurrentPopup();
823
824 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
825 return pressed;
826}
827
828bool ImGui::Button(const char* label, const ImVec2& size_arg)
829{
830 return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
831}
832
833// Small buttons fits within text without additional vertical spacing.
834bool ImGui::SmallButton(const char* label)
835{
836 ImGuiContext& g = *GImGui;
837 float backup_padding_y = g.Style.FramePadding.y;
838 g.Style.FramePadding.y = 0.0f;
839 bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
840 g.Style.FramePadding.y = backup_padding_y;
841 return pressed;
842}
843
844// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
845// 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)
846bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
847{
848 ImGuiContext& g = *GImGui;
849 ImGuiWindow* window = GetCurrentWindow();
850 if (window->SkipItems)
851 return false;
852
853 // Ensure zero-size fits to contents
854 ImVec2 size = CalcItemSize(ImVec2(size_arg.x != 0.0f ? size_arg.x : -FLT_MIN, size_arg.y != 0.0f ? size_arg.y : -FLT_MIN), 0.0f, 0.0f);
855
856 const ImGuiID id = window->GetID(str_id);
857 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
858 ItemSize(size);
859 if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
860 return false;
861
862 bool hovered, held;
863 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
864 RenderNavCursor(bb, id);
865
866 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
867 return pressed;
868}
869
870bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
871{
872 ImGuiContext& g = *GImGui;
873 ImGuiWindow* window = GetCurrentWindow();
874 if (window->SkipItems)
875 return false;
876
877 const ImGuiID id = window->GetID(str_id);
878 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
879 const float default_size = GetFrameHeight();
880 ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
881 if (!ItemAdd(bb, id))
882 return false;
883
884 bool hovered, held;
885 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
886
887 // Render
888 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
889 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
890 RenderNavCursor(bb, id);
891 RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
892 RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir);
893
894 IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
895 return pressed;
896}
897
898bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
899{
900 float sz = GetFrameHeight();
901 return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
902}
903
904// Button to close a window
905bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
906{
907 ImGuiContext& g = *GImGui;
908 ImGuiWindow* window = g.CurrentWindow;
909
910 // 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)
911 // 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?
912 const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
913 ImRect bb_interact = bb;
914 const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
915 if (area_to_visible_ratio < 1.5f)
916 bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
917
918 // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
919 // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer).
920 bool is_clipped = !ItemAdd(bb_interact, id);
921
922 bool hovered, held;
923 bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
924 if (is_clipped)
925 return pressed;
926
927 // Render
928 ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
929 if (hovered)
930 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
931 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
932 const ImU32 cross_col = GetColorU32(ImGuiCol_Text);
933 const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
934 const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
935 const float cross_thickness = 1.0f * (float)(int)g.Style._MainScale; // FIXME-DPI
936 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness);
937 window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness);
938
939 return pressed;
940}
941
942// The Collapse button also functions as a Dock Menu button.
943bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
944{
945 ImGuiContext& g = *GImGui;
946 ImGuiWindow* window = g.CurrentWindow;
947
948 ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
949 bool is_clipped = !ItemAdd(bb, id);
950 bool hovered, held;
951 bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
952 if (is_clipped)
953 return pressed;
954
955 // Render
956 //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
957 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
958 ImU32 text_col = GetColorU32(ImGuiCol_Text);
959 if (hovered || held)
960 window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
961 RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
962
963 if (dock_node)
964 RenderArrowDockMenu(window->DrawList, bb.Min, g.FontSize, text_col);
965 else
966 RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
967
968 // Switch to moving the window after mouse is moved beyond the initial drag threshold
969 if (IsItemActive() && IsMouseDragging(0))
970 StartMouseMovingWindowOrNode(window, dock_node, true); // Undock from window/collapse menu button
971
972 return pressed;
973}
974
975ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
976{
977 return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
978}
979
980// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
981ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
982{
983 ImGuiContext& g = *GImGui;
984 const ImRect outer_rect = window->Rect();
985 const ImRect inner_rect = window->InnerRect;
986 const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
987 IM_ASSERT(scrollbar_size >= 0.0f);
988 const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
989 const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : (window->Flags & ImGuiWindowFlags_NoTitleBar) ? border_size : 0;
990 if (axis == ImGuiAxis_X)
991 return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
992 else
993 return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y + border_top, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size);
994}
995
996void ImGui::ExtendHitBoxWhenNearViewportEdge(ImGuiWindow* window, ImRect* bb, float threshold, ImGuiAxis axis)
997{
998 ImRect window_rect = window->RootWindow->Rect();
999 ImRect viewport_rect = window->Viewport->GetMainRect();
1000 if (window_rect.Min[axis] == viewport_rect.Min[axis] && bb->Min[axis] > window_rect.Min[axis] && bb->Min[axis] - threshold <= window_rect.Min[axis])
1001 bb->Min[axis] = window_rect.Min[axis];
1002 if (window_rect.Max[axis] == viewport_rect.Max[axis] && bb->Max[axis] < window_rect.Max[axis] && bb->Max[axis] + threshold >= window_rect.Max[axis])
1003 bb->Max[axis] = window_rect.Max[axis];
1004}
1005
1006void ImGui::Scrollbar(ImGuiAxis axis)
1007{
1008 ImGuiContext& g = *GImGui;
1009 ImGuiWindow* window = g.CurrentWindow;
1010 const ImGuiID id = GetWindowScrollbarID(window, axis);
1011
1012 // Calculate scrollbar bounding box
1013 ImRect bb = GetWindowScrollbarRect(window, axis);
1014 ImRect host_rect = (window->DockIsActive ? window->DockNode->HostWindow : window)->Rect();
1015 ImDrawFlags rounding_corners = CalcRoundingFlagsForRectInRect(bb, host_rect, g.Style.WindowBorderSize);
1016 float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
1017 float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
1018 ImS64 scroll = (ImS64)window->Scroll[axis];
1019 ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
1020 window->Scroll[axis] = (float)scroll;
1021}
1022
1023// Vertical/Horizontal scrollbar
1024// The entire piece of code below is rather confusing because:
1025// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
1026// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
1027// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
1028// Still, the code should probably be made simpler..
1029bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags draw_rounding_flags)
1030{
1031 ImGuiContext& g = *GImGui;
1032 ImGuiWindow* window = g.CurrentWindow;
1033 if (window->SkipItems)
1034 return false;
1035
1036 const float bb_frame_width = bb_frame.GetWidth();
1037 const float bb_frame_height = bb_frame.GetHeight();
1038 if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
1039 return false;
1040
1041 // 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)
1042 float alpha = 1.0f;
1043 if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
1044 alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f));
1045 if (alpha <= 0.0f)
1046 return false;
1047
1048 const ImGuiStyle& style = g.Style;
1049 const bool allow_interaction = (alpha >= 1.0f);
1050
1051 ImRect bb = bb_frame;
1052 float padding = IM_TRUNC(ImMin(style.ScrollbarPadding, ImMin(bb_frame_width, bb_frame_height) * 0.5f));
1053 bb.Expand(-padding);
1054
1055 // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
1056 const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
1057 if (scrollbar_size_v < 1.0f)
1058 return false;
1059
1060 // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
1061 // But we maintain a minimum size in pixel to allow for the user to still aim inside.
1062 IM_ASSERT(ImMax(size_contents_v, size_visible_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers.
1063 const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
1064 const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize);
1065 const float grab_h_pixels = (float)(int)ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v);
1066 const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1067
1068 // As a special thing, we allow scrollbar near the edge of a screen/viewport to be reachable with mouse at the extreme edge (#9276)
1069 ImRect bb_hit = bb_frame;
1070 ExtendHitBoxWhenNearViewportEdge(window, &bb_hit, g.Style.WindowBorderSize, (ImGuiAxis)(axis ^ 1));
1071
1072 // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1073 bool held = false;
1074 bool hovered = false;
1075 ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
1076 ButtonBehavior(bb_hit, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
1077
1078 const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1079 float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1080 float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1081 if (held && allow_interaction && grab_h_norm < 1.0f)
1082 {
1083 const float scrollbar_pos_v = bb.Min[axis];
1084 const float mouse_pos_v = g.IO.MousePos[axis];
1085
1086 // Click position in scrollbar normalized space (0.0f->1.0f)
1087 const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1088
1089 const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1090 if (g.ActiveIdIsJustActivated)
1091 {
1092 // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1093 const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1094 g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1095 g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1096 }
1097
1098 // Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1099 // 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
1100 if (g.ScrollbarSeekMode == 0)
1101 {
1102 // Absolute seeking
1103 const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1104 *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1105 }
1106 else
1107 {
1108 // Page by page
1109 if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1110 {
1111 float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1112 *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1113 }
1114 }
1115
1116 // Update values for rendering
1117 scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1118 grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1119
1120 // Update distance to grab now that we have seek'ed and saturated
1121 //if (seek_absolute)
1122 // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1123 }
1124
1125 // Render
1126 const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1127 const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1128 window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1129 ImRect grab_rect;
1130 if (axis == ImGuiAxis_X)
1131 grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y);
1132 else
1133 grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels);
1134 window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1135
1136 return held;
1137}
1138
1139// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1140// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1141void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1142{
1143 ImGuiContext& g = *GImGui;
1144 ImGuiWindow* window = GetCurrentWindow();
1145 if (window->SkipItems)
1146 return;
1147
1148 const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1149 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1150 ItemSize(bb);
1151 if (!ItemAdd(bb, 0))
1152 return;
1153
1154 // Render
1155 float rounding = g.Style.ImageRounding;
1156 if (bg_col.w > 0.0f)
1157 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col), rounding);
1158 if (rounding > 0.0f)
1159 window->DrawList->AddImageRounded(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col), rounding);
1160 else
1161 window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1162 if (g.Style.ImageBorderSize > 0.0f)
1163 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), rounding, g.Style.ImageBorderSize);
1164}
1165
1166void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1167{
1168 ImageWithBg(tex_ref, image_size, uv0, uv1);
1169}
1170
1171// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1172#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1173void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1174{
1175 ImGuiContext& g = *GImGui;
1176 PushStyleVar(ImGuiStyleVar_ImageBorderSize, (border_col.w > 0.0f) ? ImMax(1.0f, g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f
1177 PushStyleColor(ImGuiCol_Border, border_col);
1178 ImageWithBg(tex_ref, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
1179 PopStyleColor();
1180 PopStyleVar();
1181}
1182#endif
1183
1184bool ImGui::ImageButtonEx(ImGuiID id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
1185{
1186 ImGuiContext& g = *GImGui;
1187 ImGuiWindow* window = GetCurrentWindow();
1188 if (window->SkipItems)
1189 return false;
1190
1191 const ImVec2 padding = g.Style.FramePadding;
1192 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1193 ItemSize(bb);
1194 if (!ItemAdd(bb, id))
1195 return false;
1196
1197 bool hovered, held;
1198 bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1199
1200 // Render
1201 const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1202 RenderNavCursor(bb, id);
1203 RenderFrame(bb.Min, bb.Max, col, true, g.Style.FrameRounding);
1204 if (bg_col.w > 0.0f)
1205 window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1206 float image_rounding = ImMax(g.Style.FrameRounding - ImMax(padding.x, padding.y), g.Style.ImageRounding);
1207 if (image_rounding > 0.0f)
1208 window->DrawList->AddImageRounded(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col), image_rounding);
1209 else
1210 window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1211
1212 return pressed;
1213}
1214
1215// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1216// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1217bool ImGui::ImageButton(const char* str_id, ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1218{
1219 ImGuiContext& g = *GImGui;
1220 ImGuiWindow* window = g.CurrentWindow;
1221 if (window->SkipItems)
1222 return false;
1223
1224 return ImageButtonEx(window->GetID(str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
1225}
1226
1227#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1228// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1229// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1230// - new ImageButton() requires an explicit 'const char* str_id'
1231// - old ImageButton() had frame_padding' override argument.
1232// - new ImageButton() always use style.FramePadding.
1233/*
1234bool 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)
1235{
1236 // Default to using texture ID as ID. User can still push string/integer prefixes.
1237 PushID((ImTextureID)(intptr_t)user_texture_id);
1238 if (frame_padding >= 0)
1239 PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1240 bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1241 if (frame_padding >= 0)
1242 PopStyleVar();
1243 PopID();
1244 return ret;
1245}
1246*/
1247#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1248
1249bool ImGui::Checkbox(const char* label, bool* v)
1250{
1251 ImGuiWindow* window = GetCurrentWindow();
1252 if (window->SkipItems)
1253 return false;
1254
1255 ImGuiContext& g = *GImGui;
1256 const ImGuiStyle& style = g.Style;
1257 const ImGuiID id = window->GetID(label);
1258 const char* label_end = FindRenderedTextEnd(label);
1259 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1260
1261 const float square_sz = GetFrameHeight();
1262 const ImVec2 pos = window->DC.CursorPos;
1263 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));
1264 ItemSize(total_bb, style.FramePadding.y);
1265 const bool is_visible = ItemAdd(total_bb, id);
1266 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1267 if (!is_visible)
1268 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1269 {
1270 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1271 return false;
1272 }
1273
1274 // Range-Selection/Multi-selection support (header)
1275 bool checked = *v;
1276 if (is_multi_select)
1277 MultiSelectItemHeader(id, &checked, NULL);
1278
1279 bool hovered, held;
1280 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1281
1282 // Range-Selection/Multi-selection support (footer)
1283 if (is_multi_select)
1284 MultiSelectItemFooter(id, &checked, &pressed, ImGuiMultiSelectFlags_CheckboxMode_);
1285 else if (pressed)
1286 checked = !checked;
1287
1288 if (*v != checked)
1289 {
1290 *v = checked;
1291 pressed = true; // return value
1292 MarkItemEdited(id);
1293 }
1294
1295 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1296 const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1297 if (is_visible)
1298 {
1299 RenderNavCursor(total_bb, id);
1300 ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : (mixed_value || checked) ? ImGuiCol_CheckboxSelectedBg : ImGuiCol_FrameBg);
1301 ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1302 RenderFrame(check_bb.Min, check_bb.Max, bg_col, true, style.FrameRounding);
1303 if (mixed_value)
1304 {
1305 // Undocumented tristate/mixed/indeterminate checkbox (#2644)
1306 // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1307 ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1308 window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1309 }
1310 else if (*v)
1311 {
1312 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1313 RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1314 }
1315 }
1316 const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1317 if (g.LogEnabled)
1318 LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1319 if (is_visible && label_size.x > 0.0f)
1320 RenderText(label_pos, label, label_end, false);
1321
1322 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1323 return pressed;
1324}
1325
1326template<typename T>
1327bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1328{
1329 bool all_on = (*flags & flags_value) == flags_value;
1330 bool any_on = (*flags & flags_value) != 0;
1331 bool pressed;
1332 if (!all_on && any_on)
1333 {
1334 ImGuiContext& g = *GImGui;
1335 g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1336 pressed = Checkbox(label, &all_on);
1337 }
1338 else
1339 {
1340 pressed = Checkbox(label, &all_on);
1341
1342 }
1343 if (pressed)
1344 {
1345 if (all_on)
1346 *flags |= flags_value;
1347 else
1348 *flags &= ~flags_value;
1349 }
1350 return pressed;
1351}
1352
1353bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1354{
1355 return CheckboxFlagsT(label, flags, flags_value);
1356}
1357
1358bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1359{
1360 return CheckboxFlagsT(label, flags, flags_value);
1361}
1362
1363bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1364{
1365 return CheckboxFlagsT(label, flags, flags_value);
1366}
1367
1368bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1369{
1370 return CheckboxFlagsT(label, flags, flags_value);
1371}
1372
1373bool ImGui::RadioButton(const char* label, bool active)
1374{
1375 ImGuiWindow* window = GetCurrentWindow();
1376 if (window->SkipItems)
1377 return false;
1378
1379 ImGuiContext& g = *GImGui;
1380 const ImGuiStyle& style = g.Style;
1381 const ImGuiID id = window->GetID(label);
1382 const char* label_end = FindRenderedTextEnd(label);
1383 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1384
1385 const float square_sz = GetFrameHeight();
1386 const ImVec2 pos = window->DC.CursorPos;
1387 const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1388 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));
1389 ItemSize(total_bb, style.FramePadding.y);
1390 if (!ItemAdd(total_bb, id))
1391 return false;
1392
1393 ImVec2 center = check_bb.GetCenter();
1394 center.x = IM_ROUND(center.x);
1395 center.y = IM_ROUND(center.y);
1396 const float radius = (square_sz - 1.0f) * 0.5f;
1397
1398 bool hovered, held;
1399 bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1400 if (pressed)
1401 MarkItemEdited(id);
1402
1403 RenderNavCursor(total_bb, id);
1404 const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1405 window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1406 if (active)
1407 {
1408 const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1409 window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1410 }
1411
1412 if (style.FrameBorderSize > 0.0f)
1413 {
1414 window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1415 window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1416 }
1417
1418 ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1419 if (g.LogEnabled)
1420 LogRenderedText(&label_pos, active ? "(x)" : "( )");
1421 if (label_size.x > 0.0f)
1422 RenderText(label_pos, label, label_end, false);
1423
1424 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1425 return pressed;
1426}
1427
1428// 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..
1429bool ImGui::RadioButton(const char* label, int* v, int v_button)
1430{
1431 const bool pressed = RadioButton(label, *v == v_button);
1432 if (pressed)
1433 *v = v_button;
1434 return pressed;
1435}
1436
1437// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1438void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1439{
1440 ImGuiWindow* window = GetCurrentWindow();
1441 if (window->SkipItems)
1442 return;
1443
1444 ImGuiContext& g = *GImGui;
1445 const ImGuiStyle& style = g.Style;
1446
1447 ImVec2 pos = window->DC.CursorPos;
1448 ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1449 ImRect bb(pos, pos + size);
1450 ItemSize(size, style.FramePadding.y);
1451 if (!ItemAdd(bb, 0))
1452 return;
1453
1454 // Fraction < 0.0f will display an indeterminate progress bar animation
1455 // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1456 const bool is_indeterminate = (fraction < 0.0f);
1457 if (!is_indeterminate)
1458 fraction = ImSaturate(fraction);
1459
1460 // Out of courtesy we accept a NaN fraction without crashing
1461 float fill_n0 = 0.0f;
1462 float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1463
1464 if (is_indeterminate)
1465 {
1466 const float fill_width_n = 0.2f;
1467 fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1468 fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1469 fill_n0 = ImSaturate(fill_n0);
1470 }
1471
1472 // Render
1473 RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1474 bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1475 float fill_x0 = ImLerp(bb.Min.x, bb.Max.x, fill_n0);
1476 float fill_x1 = ImLerp(bb.Min.x, bb.Max.x, fill_n1);
1477 if (fill_x0 < fill_x1)
1478 RenderRectFilledInRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_x0, fill_x1, style.FrameRounding);
1479
1480 // Default displaying the fraction as percentage string, but user can override it
1481 // Don't display text for indeterminate bars by default
1482 char overlay_buf[32];
1483 if (!is_indeterminate || overlay != NULL)
1484 {
1485 if (!overlay)
1486 {
1487 ImFormatString(overlay_buf, IM_COUNTOF(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1488 overlay = overlay_buf;
1489 }
1490
1491 ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1492 if (overlay_size.x > 0.0f)
1493 {
1494 float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : fill_x1 + style.ItemSpacing.x;
1495 RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb);
1496 }
1497 }
1498}
1499
1500void ImGui::Bullet()
1501{
1502 ImGuiWindow* window = GetCurrentWindow();
1503 if (window->SkipItems)
1504 return;
1505
1506 ImGuiContext& g = *GImGui;
1507 const ImGuiStyle& style = g.Style;
1508 const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1509 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1510 ItemSize(bb);
1511 if (!ItemAdd(bb, 0))
1512 {
1513 SameLine(0, style.FramePadding.x * 2);
1514 return;
1515 }
1516
1517 // Render and stay on same line
1518 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1519 RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1520 SameLine(0, style.FramePadding.x * 2.0f);
1521}
1522
1523// This is provided as a convenience for being an often requested feature.
1524// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1525// Because of this we currently don't provide many styling options for this widget
1526// (e.g. hovered/active colors are automatically inferred from a single color).
1527bool ImGui::TextLink(const char* label)
1528{
1529 ImGuiWindow* window = GetCurrentWindow();
1530 if (window->SkipItems)
1531 return false;
1532
1533 ImGuiContext& g = *GImGui;
1534 const ImGuiID id = window->GetID(label);
1535 const char* label_end = FindRenderedTextEnd(label);
1536
1537 ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1538 ImVec2 size = CalcTextSize(label, label_end, false);
1539 ImRect bb(pos, pos + size);
1540 ItemSize(size, 0.0f);
1541 if (!ItemAdd(bb, id))
1542 return false;
1543
1544 bool hovered, held;
1545 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1546 RenderNavCursor(bb, id);
1547
1548 if (hovered)
1549 SetMouseCursor(ImGuiMouseCursor_Hand);
1550
1551 ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1552 ImVec4 line_colf = text_colf;
1553 {
1554 // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1555 // as we are currently experimenting/planning a different styling system.
1556 float h, s, v;
1557 ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1558 if (held || hovered)
1559 {
1560 v = ImSaturate(v + (held ? 0.4f : 0.3f));
1561 h = ImFmod(h + 0.02f, 1.0f);
1562 }
1563 ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1564 v = ImSaturate(v - 0.20f);
1565 ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1566 }
1567
1568 float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
1569 window->DrawList->AddLineH(bb.Min.x, bb.Max.x, line_y, GetColorU32(line_colf), 1.0f * (float)(int)g.Style._MainScale); // FIXME-TEXT: Underline mode // FIXME-DPI
1570
1571 PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1572 RenderText(bb.Min, label, label_end, false);
1573 PopStyleColor();
1574
1575 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1576 return pressed;
1577}
1578
1579bool ImGui::TextLinkOpenURL(const char* label, const char* url)
1580{
1581 ImGuiContext& g = *GImGui;
1582 if (url == NULL)
1583 url = label;
1584 bool pressed = TextLink(label);
1585 if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
1586 g.PlatformIO.Platform_OpenInShellFn(&g, url);
1587 SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when it is the same as the label.
1588 if (BeginPopupContextItem())
1589 {
1590 if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1591 SetClipboardText(url);
1592 EndPopup();
1593 }
1594 return pressed;
1595}
1596
1597//-------------------------------------------------------------------------
1598// [SECTION] Widgets: Low-level Layout helpers
1599//-------------------------------------------------------------------------
1600// - Spacing()
1601// - Dummy()
1602// - NewLine()
1603// - AlignTextToFramePadding()
1604// - SeparatorEx() [Internal]
1605// - Separator()
1606// - SplitterBehavior() [Internal]
1607// - ShrinkWidths() [Internal]
1608//-------------------------------------------------------------------------
1609
1610void ImGui::Spacing()
1611{
1612 ImGuiWindow* window = GetCurrentWindow();
1613 if (window->SkipItems)
1614 return;
1615 ItemSize(ImVec2(0, 0));
1616}
1617
1618void ImGui::Dummy(const ImVec2& size)
1619{
1620 ImGuiWindow* window = GetCurrentWindow();
1621 if (window->SkipItems)
1622 return;
1623
1624 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1625 ItemSize(size);
1626 ItemAdd(bb, 0);
1627}
1628
1629void ImGui::NewLine()
1630{
1631 ImGuiWindow* window = GetCurrentWindow();
1632 if (window->SkipItems)
1633 return;
1634
1635 ImGuiContext& g = *GImGui;
1636 const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1637 window->DC.LayoutType = ImGuiLayoutType_Vertical;
1638 window->DC.IsSameLine = false;
1639 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.
1640 ItemSize(ImVec2(0, 0));
1641 else
1642 ItemSize(ImVec2(0.0f, g.FontSize));
1643 window->DC.LayoutType = backup_layout_type;
1644}
1645
1646void ImGui::AlignTextToFramePadding()
1647{
1648 ImGuiWindow* window = GetCurrentWindow();
1649 if (window->SkipItems)
1650 return;
1651
1652 ImGuiContext& g = *GImGui;
1653 window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1654 window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1655}
1656
1657// Horizontal/vertical separating line
1658// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1659// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1660void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1661{
1662 ImGuiWindow* window = GetCurrentWindow();
1663 if (window->SkipItems)
1664 return;
1665
1666 ImGuiContext& g = *GImGui;
1667 IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1668 IM_ASSERT(thickness > 0.0f);
1669
1670 if (flags & ImGuiSeparatorFlags_Vertical)
1671 {
1672 // Vertical separator, for menu bars (use current line height).
1673 float y1 = window->DC.CursorPos.y;
1674 float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1675 const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1676 ItemSize(ImVec2(thickness, 0.0f));
1677 if (!ItemAdd(bb, 0))
1678 return;
1679
1680 // Draw
1681 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1682 if (g.LogEnabled)
1683 LogText(" |");
1684 }
1685 else if (flags & ImGuiSeparatorFlags_Horizontal)
1686 {
1687 // Horizontal Separator
1688 float x1 = window->DC.CursorPos.x;
1689 float x2 = window->WorkRect.Max.x;
1690
1691 // Preserve legacy behavior inside Columns()
1692 // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1693 // We currently don't need to provide the same feature for tables because tables naturally have border features.
1694 ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1695 if (columns)
1696 {
1697 x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1698 x2 = window->Pos.x + window->Size.x;
1699 PushColumnsBackground();
1700 }
1701
1702 // We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1703 // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1704
1705 // Between 1.71 and 1.92.7, we maintained a hack where a 1.0f thin Separator() would not impact layout.
1706 // This was mostly chosen to allow backward compatibility with user's code assuming zero-height when calculating height for layout (e.g. bottom alignment of a status bar).
1707 // In order to handle scaling we need to scale separator thickness and it would not makes sense to have a disparity depending on height.
1708 ////float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change.
1709 const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1710 ItemSize(ImVec2(0.0f, thickness));
1711
1712 if (ItemAdd(bb, 0))
1713 {
1714 // Draw
1715 window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1716 if (g.LogEnabled)
1717 LogRenderedText(&bb.Min, "--------------------------------\n");
1718
1719 }
1720 if (columns)
1721 {
1722 PopColumnsBackground();
1723 columns->LineMinY = window->DC.CursorPos.y;
1724 }
1725 }
1726}
1727
1728void ImGui::Separator()
1729{
1730 ImGuiContext& g = *GImGui;
1731 ImGuiWindow* window = g.CurrentWindow;
1732 if (window->SkipItems)
1733 return;
1734
1735 // Those flags should eventually be configurable by the user
1736 ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1737
1738 // Only applies to legacy Columns() api as they relied on Separator() a lot.
1739 if (window->DC.CurrentColumns)
1740 flags |= ImGuiSeparatorFlags_SpanAllColumns;
1741
1742 SeparatorEx(flags, ImMax(g.Style.SeparatorSize, 1.0f));
1743}
1744
1745void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1746{
1747 ImGuiContext& g = *GImGui;
1748 ImGuiWindow* window = g.CurrentWindow;
1749 ImGuiStyle& style = g.Style;
1750
1751 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1752 const ImVec2 pos = window->DC.CursorPos;
1753 const ImVec2 padding = style.SeparatorTextPadding;
1754
1755 const float separator_thickness = style.SeparatorTextBorderSize;
1756 const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1757 const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1758 const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1759 ItemSize(min_size, text_baseline_y);
1760 if (!ItemAdd(bb, id))
1761 return;
1762
1763 const float sep1_x1 = pos.x;
1764 const float sep2_x2 = bb.Max.x;
1765 const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.999f);
1766
1767 const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1768 const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN
1769
1770 // This allows using SameLine() to position something in the 'extra_w'
1771 window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1772
1773 const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1774 if (label_size.x > 0.0f)
1775 {
1776 const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1777 const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1778 if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1779 window->DrawList->AddLineH(sep1_x1, sep1_x2, seps_y, separator_col, separator_thickness);
1780 if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1781 window->DrawList->AddLineH(sep2_x1, sep2_x2, seps_y, separator_col, separator_thickness);
1782 if (g.LogEnabled)
1783 LogSetNextTextDecoration("---", NULL);
1784 RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
1785 }
1786 else
1787 {
1788 if (g.LogEnabled)
1789 LogText("---");
1790 if (separator_thickness > 0.0f)
1791 window->DrawList->AddLineH(sep1_x1, sep2_x2, seps_y, separator_col, separator_thickness);
1792 }
1793}
1794
1795void ImGui::SeparatorText(const char* label)
1796{
1797 ImGuiWindow* window = GetCurrentWindow();
1798 if (window->SkipItems)
1799 return;
1800
1801 // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1802 // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1803 // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1804 // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1805 // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1806 // and then we can turn this into a format function.
1807 SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1808}
1809
1810// 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.
1811bool 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)
1812{
1813 ImGuiContext& g = *GImGui;
1814 ImGuiWindow* window = g.CurrentWindow;
1815
1816 if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1817 return false;
1818
1819 // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1820 // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1821 // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1822 ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1823#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1824 button_flags |= ImGuiButtonFlags_AllowOverlap;
1825#endif
1826
1827 bool hovered, held;
1828 ImRect bb_interact = bb;
1829 bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1830 ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1831 if (hovered)
1832 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1833
1834 if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1835 SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1836
1837 ImRect bb_render = bb;
1838 if (held)
1839 {
1840 float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1841
1842 // Minimum pane size
1843 float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1844 float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1845 if (mouse_delta < -size_1_maximum_delta)
1846 mouse_delta = -size_1_maximum_delta;
1847 if (mouse_delta > size_2_maximum_delta)
1848 mouse_delta = size_2_maximum_delta;
1849
1850 // Apply resize
1851 if (mouse_delta != 0.0f)
1852 {
1853 *size1 = ImMax(*size1 + mouse_delta, min_size1);
1854 *size2 = ImMax(*size2 - mouse_delta, min_size2);
1855 bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1856 MarkItemEdited(id);
1857 }
1858 }
1859
1860 // Render at new position
1861 if (bg_col & IM_COL32_A_MASK)
1862 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1863 const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1864 window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1865
1866 return held;
1867}
1868
1869static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1870{
1871 const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1872 const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1873 if (int d = (int)(b->Width - a->Width))
1874 return d;
1875 return b->Index - a->Index;
1876}
1877
1878// Shrink excess width from a set of item, by removing width from the larger items first.
1879// Set items Width to -1.0f to disable shrinking this item.
1880void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min)
1881{
1882 if (count == 1)
1883 {
1884 if (items[0].Width >= 0.0f)
1885 items[0].Width = ImMax(items[0].Width - width_excess, width_min);
1886 return;
1887 }
1888 ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); // Sort largest first, smallest last.
1889 int count_same_width = 1;
1890 while (width_excess > 0.001f && count_same_width < count)
1891 {
1892 while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1893 count_same_width++;
1894 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);
1895 max_width_to_remove_per_item = ImMin(items[0].Width - width_min, max_width_to_remove_per_item);
1896 if (max_width_to_remove_per_item <= 0.0f)
1897 break;
1898 float base_width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1899 for (int item_n = 0; item_n < count_same_width; item_n++)
1900 {
1901 float width_to_remove_for_this_item = ImMin(base_width_to_remove_per_item, items[item_n].Width - width_min);
1902 items[item_n].Width -= width_to_remove_for_this_item;
1903 width_excess -= width_to_remove_for_this_item;
1904 }
1905 }
1906
1907 // Round width and redistribute remainder
1908 // 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.
1909 width_excess = 0.0f;
1910 for (int n = 0; n < count; n++)
1911 {
1912 float width_rounded = ImTrunc(items[n].Width);
1913 width_excess += items[n].Width - width_rounded;
1914 items[n].Width = width_rounded;
1915 }
1916 while (width_excess > 0.0f)
1917 for (int n = 0; n < count && width_excess > 0.0f; n++)
1918 {
1919 float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1920 items[n].Width += width_to_add;
1921 width_excess -= width_to_add;
1922 }
1923}
1924
1925//-------------------------------------------------------------------------
1926// [SECTION] Widgets: ComboBox
1927//-------------------------------------------------------------------------
1928// - CalcMaxPopupHeightFromItemCount() [Internal]
1929// - BeginCombo()
1930// - BeginComboPopup() [Internal]
1931// - EndCombo()
1932// - BeginComboPreview() [Internal]
1933// - EndComboPreview() [Internal]
1934// - Combo()
1935//-------------------------------------------------------------------------
1936
1937static float CalcMaxPopupHeightFromItemCount(int items_count)
1938{
1939 ImGuiContext& g = *GImGui;
1940 if (items_count <= 0)
1941 return FLT_MAX;
1942 return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1943}
1944
1945bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1946{
1947 ImGuiContext& g = *GImGui;
1948 ImGuiWindow* window = GetCurrentWindow();
1949
1950 ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1951 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1952 if (window->SkipItems)
1953 return false;
1954
1955 const ImGuiStyle& style = g.Style;
1956 const ImGuiID id = window->GetID(label);
1957 IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1958 if (flags & ImGuiComboFlags_WidthFitPreview)
1959 IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1960
1961 const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1962 const char* label_end = FindRenderedTextEnd(label);
1963 const ImVec2 label_size = CalcTextSize(label, label_end, false);
1964 const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, false).x : 0.0f;
1965 const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1966 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1967 const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1968 ItemSize(total_bb, style.FramePadding.y);
1969 if (!ItemAdd(total_bb, id, &bb))
1970 return false;
1971
1972 // Open on click
1973 bool hovered, held;
1974 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1975 const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1976 bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1977 if (pressed && !popup_open)
1978 {
1979 OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1980 popup_open = true;
1981 }
1982
1983 // Render shape
1984 const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1985 const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1986 RenderNavCursor(bb, id);
1987 if (!(flags & ImGuiComboFlags_NoPreview))
1988 window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1989 if (!(flags & ImGuiComboFlags_NoArrowButton))
1990 {
1991 ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1992 ImU32 text_col = GetColorU32(ImGuiCol_Text);
1993 window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1994 if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1995 RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1996 }
1997 RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1998
1999 // Custom preview
2000 if (flags & ImGuiComboFlags_CustomPreview)
2001 {
2002 g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
2003 IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
2004 preview_value = NULL;
2005 }
2006
2007 // Render preview and label
2008 if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
2009 {
2010 if (g.LogEnabled)
2011 LogSetNextTextDecoration("{", "}");
2012 RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
2013 }
2014 if (label_size.x > 0)
2015 RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label, label_end, false);
2016
2017 if (!popup_open)
2018 return false;
2019
2020 g.NextWindowData.HasFlags = backup_next_window_data_flags;
2021 return BeginComboPopup(popup_id, bb, flags);
2022}
2023
2024bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
2025{
2026 ImGuiContext& g = *GImGui;
2027 if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
2028 {
2029 g.NextWindowData.ClearFlags();
2030 return false;
2031 }
2032
2033 // Set popup size
2034 float w = bb.GetWidth();
2035 if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
2036 {
2037 g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
2038 }
2039 else
2040 {
2041 if ((flags & ImGuiComboFlags_HeightMask_) == 0)
2042 flags |= ImGuiComboFlags_HeightRegular;
2043 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
2044 int popup_max_height_in_items = -1;
2045 if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
2046 else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
2047 else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
2048 ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
2049 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
2050 constraint_min.x = w;
2051 if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
2052 constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
2053 SetNextWindowSizeConstraints(constraint_min, constraint_max);
2054 }
2055
2056 // This is essentially a specialized version of BeginPopupEx()
2057 char name[16];
2058 ImFormatString(name, IM_COUNTOF(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
2059
2060 // Set position given a custom constraint (peak into expected window size so we can position it)
2061 // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
2062 // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
2063 if (ImGuiWindow* popup_window = FindWindowByName(name))
2064 if (popup_window->WasActive)
2065 {
2066 // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
2067 ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
2068 popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
2069 ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
2070 ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
2071 SetNextWindowPos(pos);
2072 }
2073
2074 // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
2075 ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
2076 PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
2077 bool ret = Begin(name, NULL, window_flags);
2078 PopStyleVar();
2079 if (!ret)
2080 {
2081 EndPopup();
2082 if (!g.IO.ConfigDebugBeginReturnValueOnce && !g.IO.ConfigDebugBeginReturnValueLoop) // Begin may only return false with those debug tools activated.
2083 IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
2084 return false;
2085 }
2086 g.BeginComboDepth++;
2087 return true;
2088}
2089
2090void ImGui::EndCombo()
2091{
2092 ImGuiContext& g = *GImGui;
2093 g.BeginComboDepth--;
2094 char name[16];
2095 ImFormatString(name, IM_COUNTOF(name), "##Combo_%02d", g.BeginComboDepth); // FIXME: Move those to helpers?
2096 if (strcmp(g.CurrentWindow->Name, name) != 0)
2097 IM_ASSERT_USER_ERROR_RET(0, "Calling EndCombo() in wrong window!");
2098 EndPopup();
2099}
2100
2101// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2102// (Experimental, see GitHub issues: #1658, #4168)
2103bool ImGui::BeginComboPreview()
2104{
2105 ImGuiContext& g = *GImGui;
2106 ImGuiWindow* window = g.CurrentWindow;
2107 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2108
2109 if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2110 return false;
2111 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?
2112 if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
2113 return false;
2114
2115 // FIXME: This could be contained in a PushWorkRect() api
2116 preview_data->BackupCursorPos = window->DC.CursorPos;
2117 preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2118 preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2119 preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2120 preview_data->BackupLayout = window->DC.LayoutType;
2121 window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2122 window->DC.CursorMaxPos = window->DC.CursorPos;
2123 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2124 window->DC.IsSameLine = false;
2125 PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2126
2127 return true;
2128}
2129
2130void ImGui::EndComboPreview()
2131{
2132 ImGuiContext& g = *GImGui;
2133 ImGuiWindow* window = g.CurrentWindow;
2134 ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2135
2136 // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2137 ImDrawList* draw_list = window->DrawList;
2138 if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2139 if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2140 {
2141 draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2142 draw_list->_TryMergeDrawCmds();
2143 }
2144 PopClipRect();
2145 window->DC.CursorPos = preview_data->BackupCursorPos;
2146 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2147 window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2148 window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2149 window->DC.LayoutType = preview_data->BackupLayout;
2150 window->DC.IsSameLine = false;
2151 preview_data->PreviewRect = ImRect();
2152}
2153
2154// Getter for the old Combo() API: const char*[]
2155static const char* Items_ArrayGetter(void* data, int idx)
2156{
2157 const char* const* items = (const char* const*)data;
2158 return items[idx];
2159}
2160
2161// Getter for the old Combo() API: "item1\0item2\0item3\0"
2162static const char* Items_SingleStringGetter(void* data, int idx)
2163{
2164 const char* items_separated_by_zeros = (const char*)data;
2165 int items_count = 0;
2166 const char* p = items_separated_by_zeros;
2167 while (*p)
2168 {
2169 if (idx == items_count)
2170 break;
2171 p += ImStrlen(p) + 1;
2172 items_count++;
2173 }
2174 return *p ? p : NULL;
2175}
2176
2177// Old API, prefer using BeginCombo() nowadays if you can.
2178bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items)
2179{
2180 ImGuiContext& g = *GImGui;
2181
2182 // Call the getter to obtain the preview string which is a parameter to BeginCombo()
2183 const char* preview_value = NULL;
2184 if (*current_item >= 0 && *current_item < items_count)
2185 preview_value = getter(user_data, *current_item);
2186
2187 // 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.
2188 if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2189 SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2190
2191 if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2192 return false;
2193
2194 // Display items
2195 bool value_changed = false;
2196 ImGuiListClipper clipper;
2197 clipper.Begin(items_count);
2198 clipper.IncludeItemByIndex(*current_item);
2199 while (clipper.Step())
2200 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2201 {
2202 const char* item_text = getter(user_data, i);
2203 if (item_text == NULL)
2204 item_text = "*Unknown item*";
2205
2206 PushID(i);
2207 const bool item_selected = (i == *current_item);
2208 if (Selectable(item_text, item_selected) && *current_item != i)
2209 {
2210 value_changed = true;
2211 *current_item = i;
2212 }
2213 if (item_selected)
2214 SetItemDefaultFocus();
2215 PopID();
2216 }
2217
2218 EndCombo();
2219 if (value_changed)
2220 MarkItemEdited(g.LastItemData.ID);
2221
2222 return value_changed;
2223}
2224
2225// Combo box helper allowing to pass an array of strings.
2226bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2227{
2228 const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2229 return value_changed;
2230}
2231
2232// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2233bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2234{
2235 int items_count = 0;
2236 const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2237 while (*p)
2238 {
2239 p += ImStrlen(p) + 1;
2240 items_count++;
2241 }
2242 bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2243 return value_changed;
2244}
2245
2246//-------------------------------------------------------------------------
2247// [SECTION] Data Type and Data Formatting Helpers [Internal]
2248//-------------------------------------------------------------------------
2249// - DataTypeGetInfo()
2250// - DataTypeFormatString()
2251// - DataTypeApplyOp()
2252// - DataTypeApplyFromText()
2253// - DataTypeCompare()
2254// - DataTypeClamp()
2255// - GetMinimumStepAtDecimalPrecision
2256// - RoundScalarWithFormat<>()
2257//-------------------------------------------------------------------------
2258
2259static const ImU32 GDefaultRgbaColorMarkers[4] =
2260{
2261 IM_COL32(240,20,20,255), IM_COL32(20,240,20,255), IM_COL32(20,20,240,255), IM_COL32(140,140,140,255)
2262};
2263
2264static const ImGuiDataTypeInfo GDataTypeInfo[] =
2265{
2266 { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2267 { sizeof(unsigned char), "U8", "%u", "%u" },
2268 { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2269 { sizeof(unsigned short), "U16", "%u", "%u" },
2270 { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2271 { sizeof(unsigned int), "U32", "%u", "%u" },
2272#ifdef _MSC_VER
2273 { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2274 { sizeof(ImU64), "U64", "%I64u","%I64u" },
2275#else
2276 { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2277 { sizeof(ImU64), "U64", "%llu", "%llu" },
2278#endif
2279 { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2280 { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2281 { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2282 { 0, "char*","%s", "%s" }, // ImGuiDataType_String
2283};
2284IM_STATIC_ASSERT(IM_COUNTOF(GDataTypeInfo) == ImGuiDataType_COUNT);
2285
2286const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2287{
2288 IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2289 return &GDataTypeInfo[data_type];
2290}
2291
2292int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2293{
2294 // Signedness doesn't matter when pushing integer arguments
2295 if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2296 return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2297 if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2298 return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2299 if (data_type == ImGuiDataType_Float)
2300 return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2301 if (data_type == ImGuiDataType_Double)
2302 return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2303 if (data_type == ImGuiDataType_S8)
2304 return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2305 if (data_type == ImGuiDataType_U8)
2306 return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2307 if (data_type == ImGuiDataType_S16)
2308 return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2309 if (data_type == ImGuiDataType_U16)
2310 return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2311 IM_ASSERT(0);
2312 return 0;
2313}
2314
2315void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2316{
2317 IM_ASSERT(op == '+' || op == '-');
2318 switch (data_type)
2319 {
2320 case ImGuiDataType_S8:
2321 if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2322 if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2323 return;
2324 case ImGuiDataType_U8:
2325 if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2326 if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2327 return;
2328 case ImGuiDataType_S16:
2329 if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2330 if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2331 return;
2332 case ImGuiDataType_U16:
2333 if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2334 if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2335 return;
2336 case ImGuiDataType_S32:
2337 if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2338 if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2339 return;
2340 case ImGuiDataType_U32:
2341 if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2342 if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2343 return;
2344 case ImGuiDataType_S64:
2345 if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2346 if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2347 return;
2348 case ImGuiDataType_U64:
2349 if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2350 if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2351 return;
2352 case ImGuiDataType_Float:
2353 if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2354 if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2355 return;
2356 case ImGuiDataType_Double:
2357 if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2358 if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2359 return;
2360 case ImGuiDataType_COUNT: break;
2361 }
2362 IM_ASSERT(0);
2363}
2364
2365// User can input math operators (e.g. +100) to edit a numerical values.
2366// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2367bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2368{
2369 // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2370 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2371 ImGuiDataTypeStorage data_backup;
2372 memcpy(&data_backup, p_data, type_info->Size);
2373
2374 while (ImCharIsBlankA(*buf))
2375 buf++;
2376 if (!buf[0])
2377 {
2378 if (p_data_when_empty != NULL)
2379 {
2380 memcpy(p_data, p_data_when_empty, type_info->Size);
2381 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2382 }
2383 return false;
2384 }
2385
2386 // Sanitize format
2387 // - 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
2388 char format_sanitized[32];
2389 if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2390 {
2391 format = type_info->ScanFmt;
2392 }
2393 else
2394 {
2395 format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_COUNTOF(format_sanitized));
2396 if (format[0] == '\0')
2397 format = type_info->ScanFmt; // Format doesn't want us to show the number currently, but we still need to parse the resulting input
2398 }
2399
2400 // Small types need a 32-bit buffer to receive the result from scanf()
2401 int v32 = 0;
2402 if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2403 return false;
2404 if (type_info->Size < 4)
2405 {
2406 if (data_type == ImGuiDataType_S8)
2407 *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2408 else if (data_type == ImGuiDataType_U8)
2409 *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2410 else if (data_type == ImGuiDataType_S16)
2411 *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2412 else if (data_type == ImGuiDataType_U16)
2413 *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2414 else
2415 IM_ASSERT(0);
2416 }
2417
2418 return memcmp(&data_backup, p_data, type_info->Size) != 0;
2419}
2420
2421template<typename T>
2422static int DataTypeCompareT(const T* lhs, const T* rhs)
2423{
2424 if (*lhs < *rhs) return -1;
2425 if (*lhs > *rhs) return +1;
2426 return 0;
2427}
2428
2429int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2430{
2431 switch (data_type)
2432 {
2433 case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2434 case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2435 case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2436 case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2437 case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2438 case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2439 case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2440 case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2441 case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2442 case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2443 case ImGuiDataType_COUNT: break;
2444 }
2445 IM_ASSERT(0);
2446 return 0;
2447}
2448
2449template<typename T>
2450static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2451{
2452 // Clamp, both sides are optional, return true if modified
2453 if (v_min && *v < *v_min) { *v = *v_min; return true; }
2454 if (v_max && *v > *v_max) { *v = *v_max; return true; }
2455 return false;
2456}
2457
2458bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2459{
2460 switch (data_type)
2461 {
2462 case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2463 case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2464 case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2465 case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2466 case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2467 case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2468 case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2469 case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2470 case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2471 case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2472 case ImGuiDataType_COUNT: break;
2473 }
2474 IM_ASSERT(0);
2475 return false;
2476}
2477
2478bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2479{
2480 ImGuiContext& g = *GImGui;
2481 return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2482}
2483
2484static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2485{
2486 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 };
2487 if (decimal_precision < 0)
2488 return FLT_MIN;
2489 return (decimal_precision < IM_COUNTOF(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2490}
2491
2492template<typename TYPE>
2493TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2494{
2495 IM_UNUSED(data_type);
2496 IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2497 const char* fmt_start = ImParseFormatFindStart(format);
2498 if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2499 return v;
2500
2501 // Sanitize format
2502 char fmt_sanitized[32];
2503 ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_COUNTOF(fmt_sanitized));
2504 fmt_start = fmt_sanitized;
2505
2506 // Format value with our rounding, and read back
2507 char v_str[64];
2508 ImFormatString(v_str, IM_COUNTOF(v_str), fmt_start, v);
2509 const char* p = v_str;
2510 while (*p == ' ')
2511 p++;
2512 v = (TYPE)ImAtof(p);
2513
2514 return v;
2515}
2516
2517//-------------------------------------------------------------------------
2518// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2519//-------------------------------------------------------------------------
2520// - DragBehaviorT<>() [Internal]
2521// - DragBehavior() [Internal]
2522// - DragScalar()
2523// - DragScalarN()
2524// - DragFloat()
2525// - DragFloat2()
2526// - DragFloat3()
2527// - DragFloat4()
2528// - DragFloatRange2()
2529// - DragInt()
2530// - DragInt2()
2531// - DragInt3()
2532// - DragInt4()
2533// - DragIntRange2()
2534//-------------------------------------------------------------------------
2535
2536// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2537template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2538bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2539{
2540 ImGuiContext& g = *GImGui;
2541 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2542 const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2543 const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2544 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2545 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2546
2547 // Default tweak speed
2548 if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2549 v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2550
2551 // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2552 float adjust_delta = 0.0f;
2553 if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2554 {
2555 adjust_delta = g.IO.MouseDelta[axis];
2556 if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2557 adjust_delta *= 1.0f / 100.0f;
2558 if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2559 adjust_delta *= 10.0f;
2560 }
2561 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2562 {
2563 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2564 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2565 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2566 const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2567 adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2568 v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2569 }
2570 adjust_delta *= v_speed;
2571
2572 // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2573 if (axis == ImGuiAxis_Y)
2574 adjust_delta = -adjust_delta;
2575
2576 // For logarithmic use our range is effectively 0..1 so scale the delta into that range
2577 if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2578 adjust_delta /= (float)(v_max - v_min);
2579
2580 // Clear current value on activation
2581 // 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.
2582 const bool is_just_activated = g.ActiveIdIsJustActivated;
2583 const bool is_already_past_limits_and_pushing_outward = is_bounded && !is_wrapped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f));
2584 if (is_just_activated || is_already_past_limits_and_pushing_outward)
2585 {
2586 g.DragCurrentAccum = 0.0f;
2587 g.DragCurrentAccumDirty = false;
2588 }
2589 else if (adjust_delta != 0.0f)
2590 {
2591 g.DragCurrentAccum += adjust_delta;
2592 g.DragCurrentAccumDirty = true;
2593 }
2594
2595 if (!g.DragCurrentAccumDirty)
2596 return false;
2597
2598 TYPE v_cur = *v;
2599 FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2600
2601 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2602 const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2603 if (is_logarithmic)
2604 {
2605 // 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.
2606 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2607 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2608
2609 // Convert to parametric space, apply delta, convert back
2610 float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2611 float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2612 v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2613 v_old_ref_for_accum_remainder = v_old_parametric;
2614 }
2615 else
2616 {
2617 v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2618 }
2619
2620 // Round to user desired precision based on format string
2621 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2622 v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2623
2624 // Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2625 g.DragCurrentAccumDirty = false;
2626 if (is_logarithmic)
2627 {
2628 // Convert to parametric space, apply delta, convert back
2629 float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2630 g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2631 }
2632 else
2633 {
2634 g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2635 }
2636
2637 // Lose zero sign for float/double
2638 if (v_cur == (TYPE)-0)
2639 v_cur = (TYPE)0;
2640
2641 if (*v != v_cur && is_bounded)
2642 {
2643 if (is_wrapped)
2644 {
2645 // Wrap values
2646 if (v_cur < v_min)
2647 v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2648 if (v_cur > v_max)
2649 v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2650 }
2651 else
2652 {
2653 // Clamp values + handle overflow/wrap-around for integer types.
2654 if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2655 v_cur = v_min;
2656 if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2657 v_cur = v_max;
2658 }
2659 }
2660
2661 // Apply result
2662 if (*v == v_cur)
2663 return false;
2664 *v = v_cur;
2665 return true;
2666}
2667
2668bool 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)
2669{
2670 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2671 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
2672
2673 ImGuiContext& g = *GImGui;
2674 if (g.ActiveId == id)
2675 {
2676 // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2677 if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2678 ClearActiveID();
2679 else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2680 ClearActiveID();
2681 }
2682 if (g.ActiveId != id)
2683 return false;
2684 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2685 return false;
2686
2687 switch (data_type)
2688 {
2689 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
2690 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
2691 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT<ImS32, ImS32, float>(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
2692 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT<ImU32, ImS32, float>(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
2693 case ImGuiDataType_S32: return DragBehaviorT<ImS32, ImS32, float >(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags);
2694 case ImGuiDataType_U32: return DragBehaviorT<ImU32, ImS32, float >(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags);
2695 case ImGuiDataType_S64: return DragBehaviorT<ImS64, ImS64, double>(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags);
2696 case ImGuiDataType_U64: return DragBehaviorT<ImU64, ImS64, double>(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags);
2697 case ImGuiDataType_Float: return DragBehaviorT<float, float, float >(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags);
2698 case ImGuiDataType_Double: return DragBehaviorT<double,double,double>(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags);
2699 case ImGuiDataType_COUNT: break;
2700 }
2701 IM_ASSERT(0);
2702 return false;
2703}
2704
2705// Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2706static bool TempInputIsClampEnabled(ImGuiSliderFlags flags, ImGuiDataType data_type, const void* p_min, const void* p_max)
2707{
2708 if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2709 {
2710 const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? ImGui::DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
2711 if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2712 return true;
2713 if (clamp_range_dir == 0)
2714 return ImGui::DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2715 }
2716 return false;
2717}
2718
2719// 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.
2720// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2721bool 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)
2722{
2723 ImGuiWindow* window = GetCurrentWindow();
2724 if (window->SkipItems)
2725 return false;
2726
2727 ImGuiContext& g = *GImGui;
2728 const ImGuiStyle& style = g.Style;
2729 const ImGuiID id = window->GetID(label);
2730 const float w = CalcItemWidth();
2731 const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
2732
2733 const char* label_end = FindRenderedTextEnd(label);
2734 const ImVec2 label_size = CalcTextSize(label, label_end, false);
2735 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2736 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));
2737
2738 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2739 ItemSize(total_bb, style.FramePadding.y);
2740 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2741 return false;
2742
2743 // Default format string when passing NULL
2744 if (format == NULL)
2745 format = DataTypeGetInfo(data_type)->PrintFmt;
2746
2747 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2748 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2749 if (!temp_input_is_active)
2750 {
2751 // Tabbing or Ctrl+Click on Drag turns it into an InputText
2752 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2753 const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2754 const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2755 if (make_active && (clicked || double_clicked))
2756 SetKeyOwner(ImGuiKey_MouseLeft, id);
2757 if (make_active && temp_input_allowed)
2758 if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2759 temp_input_is_active = true;
2760
2761 // (Optional) simple click (without moving) turns Drag into an InputText
2762 if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2763 if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2764 {
2765 g.NavActivateId = id;
2766 g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2767 temp_input_is_active = true;
2768 }
2769
2770 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2771 if (make_active)
2772 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
2773
2774 if (make_active && !temp_input_is_active)
2775 {
2776 SetActiveID(id, window);
2777 SetFocusID(id, window);
2778 FocusWindow(window);
2779 g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2780 }
2781 }
2782
2783 if (temp_input_is_active)
2784 {
2785 const bool clamp_enabled = TempInputIsClampEnabled(flags, data_type, p_min, p_max);
2786 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2787 }
2788
2789 // Draw frame
2790 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2791 RenderNavCursor(frame_bb, id);
2792 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
2793 if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
2794 RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
2795 RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
2796
2797 // Drag behavior
2798 const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2799 if (value_changed)
2800 MarkItemEdited(id);
2801
2802 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2803 char value_buf[64];
2804 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
2805 if (g.LogEnabled)
2806 LogSetNextTextDecoration("{", "}");
2807 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2808
2809 if (label_size.x > 0.0f)
2810 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
2811
2812 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2813 return value_changed;
2814}
2815
2816bool 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)
2817{
2818 ImGuiWindow* window = GetCurrentWindow();
2819 if (window->SkipItems)
2820 return false;
2821
2822 ImGuiContext& g = *GImGui;
2823 bool value_changed = false;
2824 BeginGroup();
2825 PushID(label);
2826 PushMultiItemsWidths(components, CalcItemWidth());
2827 size_t type_size = GDataTypeInfo[data_type].Size;
2828 for (int i = 0; i < components; i++)
2829 {
2830 PushID(i);
2831 if (i > 0)
2832 SameLine(0, g.Style.ItemInnerSpacing.x);
2833 if (flags & ImGuiSliderFlags_ColorMarkers)
2834 SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
2835 value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2836 PopID();
2837 PopItemWidth();
2838 p_data = (void*)((char*)p_data + type_size);
2839 }
2840 PopID();
2841
2842 const char* label_end = FindRenderedTextEnd(label);
2843 if (label != label_end)
2844 {
2845 SameLine(0, g.Style.ItemInnerSpacing.x);
2846 TextEx(label, label_end);
2847 }
2848
2849 EndGroup();
2850 return value_changed;
2851}
2852
2853bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2854{
2855 return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2856}
2857
2858bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2859{
2860 return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2861}
2862
2863bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2864{
2865 return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2866}
2867
2868bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2869{
2870 return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2871}
2872
2873// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2874bool 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)
2875{
2876 ImGuiWindow* window = GetCurrentWindow();
2877 if (window->SkipItems)
2878 return false;
2879
2880 ImGuiContext& g = *GImGui;
2881 PushID(label);
2882 BeginGroup();
2883 PushMultiItemsWidths(2, CalcItemWidth());
2884
2885 float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2886 float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2887 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2888 bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2889 PopItemWidth();
2890 SameLine(0, g.Style.ItemInnerSpacing.x);
2891
2892 float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2893 float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2894 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2895 value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2896 PopItemWidth();
2897 SameLine(0, g.Style.ItemInnerSpacing.x);
2898
2899 TextEx(label, FindRenderedTextEnd(label));
2900 EndGroup();
2901 PopID();
2902
2903 return value_changed;
2904}
2905
2906// NB: v_speed is float to allow adjusting the drag speed with more precision
2907bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2908{
2909 return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2910}
2911
2912bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2913{
2914 return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2915}
2916
2917bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2918{
2919 return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2920}
2921
2922bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2923{
2924 return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2925}
2926
2927// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2928bool 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)
2929{
2930 ImGuiWindow* window = GetCurrentWindow();
2931 if (window->SkipItems)
2932 return false;
2933
2934 ImGuiContext& g = *GImGui;
2935 PushID(label);
2936 BeginGroup();
2937 PushMultiItemsWidths(2, CalcItemWidth());
2938
2939 int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2940 int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2941 ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2942 bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2943 PopItemWidth();
2944 SameLine(0, g.Style.ItemInnerSpacing.x);
2945
2946 int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2947 int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2948 ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2949 value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2950 PopItemWidth();
2951 SameLine(0, g.Style.ItemInnerSpacing.x);
2952
2953 TextEx(label, FindRenderedTextEnd(label));
2954 EndGroup();
2955 PopID();
2956
2957 return value_changed;
2958}
2959
2960//-------------------------------------------------------------------------
2961// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2962//-------------------------------------------------------------------------
2963// - ScaleRatioFromValueT<> [Internal]
2964// - ScaleValueFromRatioT<> [Internal]
2965// - SliderBehaviorT<>() [Internal]
2966// - SliderBehavior() [Internal]
2967// - SliderScalar()
2968// - SliderScalarN()
2969// - SliderFloat()
2970// - SliderFloat2()
2971// - SliderFloat3()
2972// - SliderFloat4()
2973// - SliderAngle()
2974// - SliderInt()
2975// - SliderInt2()
2976// - SliderInt3()
2977// - SliderInt4()
2978// - VSliderScalar()
2979// - VSliderFloat()
2980// - VSliderInt()
2981//-------------------------------------------------------------------------
2982
2983// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2984template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2985float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2986{
2987 if (v_min == v_max)
2988 return 0.0f;
2989 IM_UNUSED(data_type);
2990
2991 const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2992 if (logarithmic_zero_epsilon > 0.0f) // == is_logarithmic from caller
2993 {
2994 bool flipped = v_max < v_min;
2995
2996 if (flipped) // Handle the case where the range is backwards
2997 ImSwap(v_min, v_max);
2998
2999 // Fudge min/max to avoid getting close to log(0)
3000 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
3001 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
3002
3003 // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3004 if ((v_min == 0.0f) && (v_max < 0.0f))
3005 v_min_fudged = -logarithmic_zero_epsilon;
3006 else if ((v_max == 0.0f) && (v_min < 0.0f))
3007 v_max_fudged = -logarithmic_zero_epsilon;
3008
3009 float result;
3010 if (v_clamped <= v_min_fudged)
3011 result = 0.0f; // Workaround for values that are in-range but below our fudge
3012 else if (v_clamped >= v_max_fudged)
3013 result = 1.0f; // Workaround for values that are in-range but above our fudge
3014 else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
3015 {
3016 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)
3017 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3018 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3019 if (v == 0.0f)
3020 result = zero_point_center; // Special case for exactly zero
3021 else if (v < 0.0f)
3022 result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
3023 else
3024 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));
3025 }
3026 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3027 result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
3028 else
3029 result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
3030
3031 return flipped ? (1.0f - result) : result;
3032 }
3033 else
3034 {
3035 // Linear slider
3036 return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
3037 }
3038}
3039
3040// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
3041template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3042TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
3043{
3044 // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
3045 // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
3046 if (t <= 0.0f || v_min == v_max)
3047 return v_min;
3048 if (t >= 1.0f)
3049 return v_max;
3050
3051 TYPE result = (TYPE)0;
3052 if (logarithmic_zero_epsilon > 0.0f) // == is_logarithmic from caller
3053 {
3054 // Fudge min/max to avoid getting silly results close to zero
3055 FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
3056 FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
3057
3058 const bool flipped = v_max < v_min; // Check if range is "backwards"
3059 if (flipped)
3060 ImSwap(v_min_fudged, v_max_fudged);
3061
3062 // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3063 if ((v_max == 0.0f) && (v_min < 0.0f))
3064 v_max_fudged = -logarithmic_zero_epsilon;
3065
3066 float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
3067
3068 if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
3069 {
3070 float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
3071 float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3072 float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3073 if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
3074 result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
3075 else if (t_with_flip < zero_point_center)
3076 result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
3077 else
3078 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))));
3079 }
3080 else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3081 result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
3082 else
3083 result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
3084 }
3085 else
3086 {
3087 // Linear slider
3088 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3089 if (is_floating_point)
3090 {
3091 result = ImLerp(v_min, v_max, t);
3092 }
3093 else if (t < 1.0)
3094 {
3095 // - For integer values we want the clicking position to match the grab box so we round above
3096 // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3097 // - 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
3098 // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3099 FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3100 result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3101 }
3102 }
3103
3104 return result;
3105}
3106
3107// FIXME: Try to move more of the code into shared SliderBehavior()
3108template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3109bool 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)
3110{
3111 ImGuiContext& g = *GImGui;
3112 const ImGuiStyle& style = g.Style;
3113
3114 const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3115 const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3116 const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3117 const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it.
3118
3119 // Calculate bounds
3120 const float grab_padding = 2.0f; // FIXME: Should be part of style.
3121 const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3122 float grab_sz = style.GrabMinSize;
3123 if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3124 grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3125 grab_sz = ImMin(grab_sz, slider_sz);
3126 const float slider_usable_sz = slider_sz - grab_sz;
3127 const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3128 const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3129
3130 float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3131 float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3132 if (is_logarithmic)
3133 {
3134 // 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.
3135 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3136 logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3137 zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3138 }
3139
3140 // Process interacting with the slider
3141 bool value_changed = false;
3142 if (g.ActiveId == id)
3143 {
3144 bool set_new_value = false;
3145 float clicked_t = 0.0f;
3146 if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3147 {
3148 if (!g.IO.MouseDown[0])
3149 {
3150 ClearActiveID();
3151 }
3152 else
3153 {
3154 const float mouse_abs_pos = g.IO.MousePos[axis];
3155 if (g.ActiveIdIsJustActivated)
3156 {
3157 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3158 if (axis == ImGuiAxis_Y)
3159 grab_t = 1.0f - grab_t;
3160 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3161 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.
3162 g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3163 }
3164 if (slider_usable_sz > 0.0f)
3165 clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3166 if (axis == ImGuiAxis_Y)
3167 clicked_t = 1.0f - clicked_t;
3168 set_new_value = true;
3169 }
3170 }
3171 else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3172 {
3173 if (g.ActiveIdIsJustActivated)
3174 {
3175 g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3176 g.SliderCurrentAccumDirty = false;
3177 }
3178
3179 float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3180 if (input_delta != 0.0f)
3181 {
3182 const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3183 const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3184 const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3185 if (decimal_precision > 0)
3186 {
3187 input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3188 if (tweak_slow)
3189 input_delta /= 10.0f;
3190 }
3191 else
3192 {
3193 if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3194 input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3195 else
3196 input_delta /= 100.0f;
3197 }
3198 if (tweak_fast)
3199 input_delta *= 10.0f;
3200
3201 g.SliderCurrentAccum += input_delta;
3202 g.SliderCurrentAccumDirty = true;
3203 }
3204
3205 float delta = g.SliderCurrentAccum;
3206 if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3207 {
3208 ClearActiveID();
3209 }
3210 else if (g.SliderCurrentAccumDirty)
3211 {
3212 clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3213
3214 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
3215 {
3216 set_new_value = false;
3217 g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3218 }
3219 else
3220 {
3221 set_new_value = true;
3222 float old_clicked_t = clicked_t;
3223 clicked_t = ImSaturate(clicked_t + delta);
3224
3225 // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3226 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3227 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3228 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3229 float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3230
3231 if (delta > 0)
3232 g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3233 else
3234 g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3235 }
3236
3237 g.SliderCurrentAccumDirty = false;
3238 }
3239 }
3240
3241 if (set_new_value)
3242 if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3243 set_new_value = false;
3244
3245 if (set_new_value)
3246 {
3247 TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3248
3249 // Round to user desired precision based on format string
3250 if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3251 v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3252
3253 // Apply result
3254 if (*v != v_new)
3255 {
3256 *v = v_new;
3257 value_changed = true;
3258 }
3259 }
3260 }
3261
3262 if (slider_sz < 1.0f)
3263 {
3264 *out_grab_bb = ImRect(bb.Min, bb.Min);
3265 }
3266 else
3267 {
3268 // Output grab position so it can be displayed by the caller
3269 float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3270 if (axis == ImGuiAxis_Y)
3271 grab_t = 1.0f - grab_t;
3272 const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3273 if (axis == ImGuiAxis_X)
3274 *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);
3275 else
3276 *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);
3277 }
3278
3279 return value_changed;
3280}
3281
3282// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3283// 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.
3284// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3285bool 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)
3286{
3287 // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3288 IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead.");
3289 IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3290
3291 switch (data_type)
3292 {
3293 case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; }
3294 case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; }
3295 case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT<ImS32, ImS32, float>(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; }
3296 case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT<ImU32, ImS32, float>(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; }
3297 case ImGuiDataType_S32:
3298 IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3299 return SliderBehaviorT<ImS32, ImS32, float >(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb);
3300 case ImGuiDataType_U32:
3301 IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3302 return SliderBehaviorT<ImU32, ImS32, float >(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb);
3303 case ImGuiDataType_S64:
3304 IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3305 return SliderBehaviorT<ImS64, ImS64, double>(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb);
3306 case ImGuiDataType_U64:
3307 IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3308 return SliderBehaviorT<ImU64, ImS64, double>(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb);
3309 case ImGuiDataType_Float:
3310 IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3311 return SliderBehaviorT<float, float, float >(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb);
3312 case ImGuiDataType_Double:
3313 IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3314 return SliderBehaviorT<double, double, double>(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb);
3315 case ImGuiDataType_COUNT: break;
3316 }
3317 IM_ASSERT(0);
3318 return false;
3319}
3320
3321// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3322// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3323bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3324{
3325 ImGuiWindow* window = GetCurrentWindow();
3326 if (window->SkipItems)
3327 return false;
3328
3329 ImGuiContext& g = *GImGui;
3330 const ImGuiStyle& style = g.Style;
3331 const ImGuiID id = window->GetID(label);
3332 const float w = CalcItemWidth();
3333 const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
3334
3335 const char* label_end = FindRenderedTextEnd(label);
3336 const ImVec2 label_size = CalcTextSize(label, label_end, false);
3337 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3338 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));
3339
3340 const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3341 ItemSize(total_bb, style.FramePadding.y);
3342 if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3343 return false;
3344
3345 // Default format string when passing NULL
3346 if (format == NULL)
3347 format = DataTypeGetInfo(data_type)->PrintFmt;
3348
3349 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3350 bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3351 if (!temp_input_is_active)
3352 {
3353 // Tabbing or Ctrl+Click on Slider turns it into an input box
3354 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3355 const bool make_active = (clicked || g.NavActivateId == id);
3356 if (make_active && clicked)
3357 SetKeyOwner(ImGuiKey_MouseLeft, id);
3358 if (make_active && temp_input_allowed)
3359 if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3360 temp_input_is_active = true;
3361
3362 // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3363 if (make_active)
3364 memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
3365
3366 if (make_active && !temp_input_is_active)
3367 {
3368 SetActiveID(id, window);
3369 SetFocusID(id, window);
3370 FocusWindow(window);
3371 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3372 }
3373 }
3374
3375 if (temp_input_is_active)
3376 {
3377 // Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3378 const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0; // Don't use TempInputIsClampEnabled()
3379 return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3380 }
3381
3382 // Draw frame
3383 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3384 RenderNavCursor(frame_bb, id);
3385 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
3386 if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
3387 RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
3388 RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
3389
3390 // Slider behavior
3391 ImRect grab_bb;
3392 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3393 if (value_changed)
3394 MarkItemEdited(id);
3395
3396 // Render grab
3397 if (grab_bb.Max.x > grab_bb.Min.x)
3398 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3399
3400 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3401 char value_buf[64];
3402 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3403 if (g.LogEnabled)
3404 LogSetNextTextDecoration("{", "}");
3405 RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3406
3407 if (label_size.x > 0.0f)
3408 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
3409
3410 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3411 return value_changed;
3412}
3413
3414// Add multiple sliders on 1 line for compact edition of multiple components
3415bool 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)
3416{
3417 ImGuiWindow* window = GetCurrentWindow();
3418 if (window->SkipItems)
3419 return false;
3420
3421 ImGuiContext& g = *GImGui;
3422 bool value_changed = false;
3423 BeginGroup();
3424 PushID(label);
3425 PushMultiItemsWidths(components, CalcItemWidth());
3426 size_t type_size = GDataTypeInfo[data_type].Size;
3427 for (int i = 0; i < components; i++)
3428 {
3429 PushID(i);
3430 if (i > 0)
3431 SameLine(0, g.Style.ItemInnerSpacing.x);
3432 if (flags & ImGuiSliderFlags_ColorMarkers)
3433 SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
3434 value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3435 PopID();
3436 PopItemWidth();
3437 v = (void*)((char*)v + type_size);
3438 }
3439 PopID();
3440
3441 const char* label_end = FindRenderedTextEnd(label);
3442 if (label != label_end)
3443 {
3444 SameLine(0, g.Style.ItemInnerSpacing.x);
3445 TextEx(label, label_end);
3446 }
3447
3448 EndGroup();
3449 return value_changed;
3450}
3451
3452bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3453{
3454 return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3455}
3456
3457bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3458{
3459 return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3460}
3461
3462bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3463{
3464 return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3465}
3466
3467bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3468{
3469 return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3470}
3471
3472bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3473{
3474 if (format == NULL)
3475 format = "%.0f deg";
3476 float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3477 bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3478 if (value_changed)
3479 *v_rad = v_deg * (2 * IM_PI) / 360.0f;
3480 return value_changed;
3481}
3482
3483bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3484{
3485 return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3486}
3487
3488bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3489{
3490 return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3491}
3492
3493bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3494{
3495 return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3496}
3497
3498bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3499{
3500 return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3501}
3502
3503bool 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)
3504{
3505 ImGuiWindow* window = GetCurrentWindow();
3506 if (window->SkipItems)
3507 return false;
3508
3509 ImGuiContext& g = *GImGui;
3510 const ImGuiStyle& style = g.Style;
3511 const ImGuiID id = window->GetID(label);
3512
3513 const char* label_end = FindRenderedTextEnd(label);
3514 const ImVec2 label_size = CalcTextSize(label, label_end, false);
3515 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3516 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));
3517
3518 ItemSize(bb, style.FramePadding.y);
3519 if (!ItemAdd(frame_bb, id))
3520 return false;
3521
3522 // Default format string when passing NULL
3523 if (format == NULL)
3524 format = DataTypeGetInfo(data_type)->PrintFmt;
3525
3526 const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3527 const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3528 if (clicked || g.NavActivateId == id)
3529 {
3530 if (clicked)
3531 SetKeyOwner(ImGuiKey_MouseLeft, id);
3532 SetActiveID(id, window);
3533 SetFocusID(id, window);
3534 FocusWindow(window);
3535 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3536 }
3537
3538 // Draw frame
3539 const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3540 RenderNavCursor(frame_bb, id);
3541 RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3542
3543 // Slider behavior
3544 ImRect grab_bb;
3545 const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3546 if (value_changed)
3547 MarkItemEdited(id);
3548
3549 // Render grab
3550 if (grab_bb.Max.y > grab_bb.Min.y)
3551 window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3552
3553 // Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3554 // For the vertical slider we allow centered text to overlap the frame padding
3555 char value_buf[64];
3556 const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3557 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f));
3558 if (label_size.x > 0.0f)
3559 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
3560
3561 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
3562 return value_changed;
3563}
3564
3565bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3566{
3567 return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3568}
3569
3570bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3571{
3572 return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3573}
3574
3575//-------------------------------------------------------------------------
3576// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3577//-------------------------------------------------------------------------
3578// - ImParseFormatFindStart() [Internal]
3579// - ImParseFormatFindEnd() [Internal]
3580// - ImParseFormatTrimDecorations() [Internal]
3581// - ImParseFormatSanitizeForPrinting() [Internal]
3582// - ImParseFormatSanitizeForScanning() [Internal]
3583// - ImParseFormatPrecision() [Internal]
3584// - TempInputText() [Internal]
3585// - TempInputScalar() [Internal]
3586// - InputScalar()
3587// - InputScalarN()
3588// - InputFloat()
3589// - InputFloat2()
3590// - InputFloat3()
3591// - InputFloat4()
3592// - InputInt()
3593// - InputInt2()
3594// - InputInt3()
3595// - InputInt4()
3596// - InputDouble()
3597//-------------------------------------------------------------------------
3598
3599// We don't use strchr() because our strings are usually very short and often start with '%'
3600const char* ImParseFormatFindStart(const char* fmt)
3601{
3602 while (char c = fmt[0])
3603 {
3604 if (c == '%' && fmt[1] != '%')
3605 return fmt;
3606 else if (c == '%')
3607 fmt++;
3608 fmt++;
3609 }
3610 return fmt;
3611}
3612
3613const char* ImParseFormatFindEnd(const char* fmt)
3614{
3615 // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3616 if (fmt[0] != '%')
3617 return fmt;
3618 const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3619 const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3620 for (char c; (c = *fmt) != 0; fmt++)
3621 {
3622 if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3623 return fmt + 1;
3624 if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3625 return fmt + 1;
3626 }
3627 return fmt;
3628}
3629
3630// Extract the format out of a format string with leading or trailing decorations
3631// fmt = "blah blah" -> return ""
3632// fmt = "%.3f" -> return fmt
3633// fmt = "hello %.3f" -> return fmt + 6
3634// fmt = "%.3f hello" -> return buf written with "%.3f"
3635const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3636{
3637 const char* fmt_start = ImParseFormatFindStart(fmt);
3638 if (fmt_start[0] != '%')
3639 return "";
3640 const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3641 if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3642 return fmt_start;
3643 ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3644 return buf;
3645}
3646
3647// Sanitize format
3648// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3649// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3650void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3651{
3652 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3653 IM_UNUSED(fmt_out_size);
3654 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3655 while (fmt_in < fmt_end)
3656 {
3657 char c = *fmt_in++;
3658 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3659 *(fmt_out++) = c;
3660 }
3661 *fmt_out = 0; // Zero-terminate
3662}
3663
3664// - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d"
3665const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3666{
3667 const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3668 const char* fmt_out_begin = fmt_out;
3669 IM_UNUSED(fmt_out_size);
3670 IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3671 bool has_type = false;
3672 while (fmt_in < fmt_end)
3673 {
3674 char c = *fmt_in++;
3675 if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3676 continue;
3677 has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3678 if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3679 *(fmt_out++) = c;
3680 }
3681 *fmt_out = 0; // Zero-terminate
3682 return fmt_out_begin;
3683}
3684
3685template<typename TYPE>
3686static const char* ImAtoi(const char* src, TYPE* output)
3687{
3688 int negative = 0;
3689 if (*src == '-') { negative = 1; src++; }
3690 if (*src == '+') { src++; }
3691 TYPE v = 0;
3692 while (*src >= '0' && *src <= '9')
3693 v = (v * 10) + (*src++ - '0');
3694 *output = negative ? -v : v;
3695 return src;
3696}
3697
3698// Parse display precision back from the display format string
3699// 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.
3700int ImParseFormatPrecision(const char* fmt, int default_precision)
3701{
3702 fmt = ImParseFormatFindStart(fmt);
3703 if (fmt[0] != '%')
3704 return default_precision;
3705 fmt++;
3706 while (*fmt >= '0' && *fmt <= '9')
3707 fmt++;
3708 int precision = INT_MAX;
3709 if (*fmt == '.')
3710 {
3711 fmt = ImAtoi<int>(fmt + 1, &precision);
3712 if (precision < 0 || precision > 99)
3713 precision = default_precision;
3714 }
3715 if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3716 precision = -1;
3717 if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3718 precision = -1;
3719 return (precision == INT_MAX) ? default_precision : precision;
3720}
3721
3722// Create text input in place of another active widget (e.g. used when doing a Ctrl+Click on drag/slider widgets)
3723// - This must be submitted right after the item it is overlaying.
3724// FIXME: Facilitate using this in variety of other situations.
3725bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3726{
3727 // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3728 // We clear ActiveID on the first frame to allow the InputText() taking it back.
3729 ImGuiContext& g = *GImGui;
3730 ImGuiWindow* window = g.CurrentWindow;
3731
3732 const bool init = (g.TempInputId != id);
3733 if (init)
3734 ClearActiveID();
3735
3736 ImVec2 backup_pos = window->DC.CursorPos;
3737 window->DC.CursorPos = bb.Min;
3738 g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; // Using ImGuiInputTextFlags_MergedItem above will skip ItemAdd() so we poke here.
3739 bool value_changed = InputTextEx(label, NULL, buf, (int)buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_TempInput | ImGuiInputTextFlags_AutoSelectAll, callback, user_data);
3740 KeepAliveID(id); // Not done because of ImGuiInputTextFlags_TempInput
3741 if (init)
3742 {
3743 // First frame we started displaying the InputText widget, we expect it to take the active id.
3744 IM_ASSERT(g.ActiveId == id);
3745 g.TempInputId = g.ActiveId;
3746 }
3747 if (g.ActiveId != id)
3748 g.TempInputId = 0;
3749 window->DC.CursorPos = backup_pos;
3750 return value_changed;
3751}
3752
3753// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3754// This is intended: this way we allow Ctrl+Click manual input to set a value out of bounds, for maximum flexibility.
3755// However this may not be ideal for all uses, as some user code may break on out of bound values.
3756bool 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)
3757{
3758 // FIXME: May need to clarify display behavior if format doesn't contain %.
3759 // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3760 ImGuiContext& g = *GImGui;
3761 const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3762 char fmt_buf[32];
3763 char data_buf[32];
3764 format = ImParseFormatTrimDecorations(format, fmt_buf, IM_COUNTOF(fmt_buf));
3765 if (format[0] == 0)
3766 format = type_info->PrintFmt;
3767 DataTypeFormatString(data_buf, IM_COUNTOF(data_buf), data_type, p_data, format);
3768 ImStrTrimBlanks(data_buf);
3769
3770 ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3771 g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3772 if (!TempInputText(bb, id, label, data_buf, IM_COUNTOF(data_buf), flags))
3773 return false;
3774
3775 // Backup old value
3776 size_t data_type_size = type_info->Size;
3777 ImGuiDataTypeStorage data_backup;
3778 memcpy(&data_backup, p_data, data_type_size);
3779
3780 // Apply new value (or operations) then clamp
3781 DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3782 if (p_clamp_min || p_clamp_max)
3783 {
3784 if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3785 ImSwap(p_clamp_min, p_clamp_max);
3786 DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3787 }
3788
3789 // Only mark as edited if new value is different
3790 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3791 bool value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3792 if (value_changed)
3793 MarkItemEdited(id);
3794 return value_changed;
3795}
3796
3797void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3798{
3799 ImGuiContext& g = *GImGui;
3800 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3801 memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3802}
3803
3804// 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.
3805// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3806bool 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)
3807{
3808 ImGuiWindow* window = GetCurrentWindow();
3809 if (window->SkipItems)
3810 return false;
3811
3812 ImGuiContext& g = *GImGui;
3813 ImGuiStyle& style = g.Style;
3814 //IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()!
3815
3816 if (format == NULL)
3817 format = DataTypeGetInfo(data_type)->PrintFmt;
3818
3819 void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3820
3821 char buf[64];
3822 if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3823 buf[0] = 0;
3824 else
3825 DataTypeFormatString(buf, IM_COUNTOF(buf), data_type, p_data, format);
3826
3827 // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3828 // We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3829 g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3830 flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3831
3832 const bool has_step_buttons = (p_step != NULL);
3833 const float button_size = has_step_buttons ? GetFrameHeight() : 0.0f;
3834 bool ret;
3835 if (has_step_buttons)
3836 {
3837 // With Step Buttons
3838 BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3839 PushID(label);
3840 SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3841 ret = InputText("", buf, IM_COUNTOF(buf), flags); // PushID(label) + "" gives us the expected ID from outside point of view
3842 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3843 }
3844 else
3845 {
3846 // Without Step Buttons
3847 ret = InputText(label, buf, IM_COUNTOF(buf), flags);
3848 }
3849
3850 // Apply
3851 bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved.
3852 bool value_changed = input_edited ? DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL) : false;
3853
3854 // Step buttons
3855 if (has_step_buttons)
3856 {
3857 const ImVec2 backup_frame_padding = style.FramePadding;
3858 style.FramePadding.x = style.FramePadding.y;
3859 if (flags & ImGuiInputTextFlags_ReadOnly)
3860 BeginDisabled();
3861 PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3862 SameLine(0, style.ItemInnerSpacing.x);
3863 if (ButtonEx("-", ImVec2(button_size, button_size)))
3864 {
3865 DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3866 value_changed = ret = true;
3867 }
3868 SameLine(0, style.ItemInnerSpacing.x);
3869 if (ButtonEx("+", ImVec2(button_size, button_size)))
3870 {
3871 DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3872 value_changed = ret = true;
3873 }
3874 PopItemFlag();
3875 if (flags & ImGuiInputTextFlags_ReadOnly)
3876 EndDisabled();
3877
3878 const char* label_end = FindRenderedTextEnd(label);
3879 if (label != label_end)
3880 {
3881 SameLine(0, style.ItemInnerSpacing.x);
3882 TextEx(label, label_end);
3883 }
3884 style.FramePadding = backup_frame_padding;
3885
3886 PopID();
3887 EndGroup();
3888 }
3889
3890 g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3891 if (value_changed)
3892 MarkItemEdited(g.LastItemData.ID);
3893
3894 if (flags & ImGuiInputTextFlags_EnterReturnsTrue)
3895 return ret;
3896 return value_changed;
3897}
3898
3899bool 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)
3900{
3901 ImGuiWindow* window = GetCurrentWindow();
3902 if (window->SkipItems)
3903 return false;
3904
3905 ImGuiContext& g = *GImGui;
3906 bool value_changed = false;
3907 BeginGroup();
3908 PushID(label);
3909 PushMultiItemsWidths(components, CalcItemWidth());
3910 size_t type_size = GDataTypeInfo[data_type].Size;
3911 for (int i = 0; i < components; i++)
3912 {
3913 PushID(i);
3914 if (i > 0)
3915 SameLine(0, g.Style.ItemInnerSpacing.x);
3916 value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3917 PopID();
3918 PopItemWidth();
3919 p_data = (void*)((char*)p_data + type_size);
3920 }
3921 PopID();
3922
3923 const char* label_end = FindRenderedTextEnd(label);
3924 if (label != label_end)
3925 {
3926 SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3927 TextEx(label, label_end);
3928 }
3929
3930 EndGroup();
3931 return value_changed;
3932}
3933
3934bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3935{
3936 return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3937}
3938
3939bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3940{
3941 return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3942}
3943
3944bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3945{
3946 return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3947}
3948
3949bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3950{
3951 return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3952}
3953
3954bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3955{
3956 // 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.
3957 const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3958 return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3959}
3960
3961bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3962{
3963 return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3964}
3965
3966bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3967{
3968 return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3969}
3970
3971bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3972{
3973 return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3974}
3975
3976bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3977{
3978 return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3979}
3980
3981//-------------------------------------------------------------------------
3982// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3983//-------------------------------------------------------------------------
3984// - imstb_textedit.h include
3985// - InputText()
3986// - InputTextWithHint()
3987// - InputTextMultiline()
3988// - InputTextEx() [Internal]
3989// - DebugNodeInputTextState() [Internal]
3990//-------------------------------------------------------------------------
3991
3992namespace ImStb
3993{
3994#include "imstb_textedit.h"
3995}
3996
3997// If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
3998bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3999{
4000 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
4001 return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
4002}
4003
4004bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
4005{
4006 return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
4007}
4008
4009bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
4010{
4011 IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
4012 return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
4013}
4014
4015static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
4016{
4017 ImGuiContext& g = *ctx;
4018 ImGuiInputTextState* obj = &g.InputTextState;
4019 IM_ASSERT(text_end_display >= text_begin && text_end_display <= text_end);
4020 return ImFontCalcTextSizeEx(g.Font, g.FontSize, FLT_MAX, obj->WrapWidth, text_begin, text_end_display, text_end, out_remaining, out_offset, flags);
4021}
4022
4023// 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)
4024// With our UTF-8 use of stb_textedit:
4025// - STB_TEXTEDIT_GETCHAR is nothing more than a a "GETBYTE". It's only used to compare to ascii or to copy blocks of text so we are fine.
4026// - One exception is the STB_TEXTEDIT_IS_SPACE feature which would expect a full char in order to handle full-width space such as 0x3000 (see ImCharIsBlankW).
4027// - ...but we don't use that feature.
4028namespace ImStb
4029{
4030static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
4031static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx >= 0 && idx <= obj->TextLen); return obj->TextSrc[idx]; }
4032static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.FontBaked->GetCharAdvance((ImWchar)c) * g.FontBakedScale; }
4033static char STB_TEXTEDIT_NEWLINE = '\n';
4034static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
4035{
4036 const char* text = obj->TextSrc;
4037 const char* text_remaining = NULL;
4038 const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepBlanks);
4039 r->x0 = 0.0f;
4040 r->x1 = size.x;
4041 r->baseline_y_delta = size.y;
4042 r->ymin = 0.0f;
4043 r->ymax = size.y;
4044 r->num_chars = (int)(text_remaining - (text + line_start_idx));
4045}
4046
4047#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
4048#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
4049
4050static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4051{
4052 if (idx >= obj->TextLen)
4053 return obj->TextLen + 1;
4054 unsigned int c;
4055 return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
4056}
4057
4058static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4059{
4060 if (idx <= 0)
4061 return -1;
4062 const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
4063 return (int)(p - obj->TextSrc);
4064}
4065
4066static bool ImCharIsSeparatorW(unsigned int c)
4067{
4068 static const unsigned int separator_list[] =
4069 {
4070 ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4071 '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4072 '\n', '\r',
4073 };
4074 for (unsigned int separator : separator_list)
4075 if (c == separator)
4076 return true;
4077 return false;
4078}
4079
4080static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4081{
4082 // 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.
4083 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4084 return 0;
4085
4086 const char* curr_p = obj->TextSrc + idx;
4087 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4088 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
4089 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);
4090
4091 bool prev_white = ImCharIsBlankW(prev_c);
4092 bool prev_separ = ImCharIsSeparatorW(prev_c);
4093 bool curr_white = ImCharIsBlankW(curr_c);
4094 bool curr_separ = ImCharIsSeparatorW(curr_c);
4095 return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4096}
4097static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4098{
4099 if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4100 return 0;
4101
4102 const char* curr_p = obj->TextSrc + idx;
4103 const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4104 unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
4105 unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);
4106
4107 bool prev_white = ImCharIsBlankW(prev_c);
4108 bool prev_separ = ImCharIsSeparatorW(prev_c);
4109 bool curr_white = ImCharIsBlankW(curr_c);
4110 bool curr_separ = ImCharIsSeparatorW(curr_c);
4111 return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4112}
4113static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4114{
4115 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4116 while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4117 idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4118 return idx < 0 ? 0 : idx;
4119}
4120static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4121{
4122 int len = obj->TextLen;
4123 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4124 while (idx < len && !is_word_boundary_from_left(obj, idx))
4125 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4126 return idx > len ? len : idx;
4127}
4128static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4129{
4130 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4131 int len = obj->TextLen;
4132 while (idx < len && !is_word_boundary_from_right(obj, idx))
4133 idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4134 return idx > len ? len : idx;
4135}
4136static 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); }
4137#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4138#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4139
4140// Reimplementation of stb_textedit_move_line_start()/stb_textedit_move_line_end() which supports word-wrapping.
4141static int STB_TEXTEDIT_MOVELINESTART_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4142{
4143 if (state->single_line)
4144 return 0;
4145
4146 if (obj->WrapWidth > 0.0f)
4147 {
4148 ImGuiContext& g = *obj->Ctx;
4149 const char* p_cursor = obj->TextSrc + cursor;
4150 const char* p_bol = ImStrbol(p_cursor, obj->TextSrc);
4151 const char* p = p_bol;
4152 const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4153 while (p >= p_bol)
4154 {
4155 const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4156 if (p == p_cursor) // If we are already on a visible beginning-of-line, return real beginning-of-line (would be same as regular handler below)
4157 return (int)(p_bol - obj->TextSrc);
4158 if (p_eol == p_cursor && obj->TextA[cursor] != '\n' && obj->LastMoveDirectionLR == ImGuiDir_Left)
4159 return (int)(p_bol - obj->TextSrc);
4160 if (p_eol >= p_cursor)
4161 return (int)(p - obj->TextSrc);
4162 p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4163 }
4164 }
4165
4166 // Regular handler, same as stb_textedit_move_line_start()
4167 while (cursor > 0)
4168 {
4169 int prev_cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, cursor);
4170 if (STB_TEXTEDIT_GETCHAR(obj, prev_cursor) == STB_TEXTEDIT_NEWLINE)
4171 break;
4172 cursor = prev_cursor;
4173 }
4174 return cursor;
4175}
4176
4177static int STB_TEXTEDIT_MOVELINEEND_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4178{
4179 int n = STB_TEXTEDIT_STRINGLEN(obj);
4180 if (state->single_line)
4181 return n;
4182
4183 if (obj->WrapWidth > 0.0f)
4184 {
4185 ImGuiContext& g = *obj->Ctx;
4186 const char* p_cursor = obj->TextSrc + cursor;
4187 const char* p = ImStrbol(p_cursor, obj->TextSrc);
4188 const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4189 while (p < text_end)
4190 {
4191 const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4192 cursor = (int)(p_eol - obj->TextSrc);
4193 if (p_eol == p_cursor && obj->LastMoveDirectionLR != ImGuiDir_Left) // If we are already on a visible end-of-line, switch to regular handle
4194 break;
4195 if (p_eol > p_cursor)
4196 return cursor;
4197 p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4198 }
4199 }
4200 // Regular handler, same as stb_textedit_move_line_end()
4201 while (cursor < n && STB_TEXTEDIT_GETCHAR(obj, cursor) != STB_TEXTEDIT_NEWLINE)
4202 cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, cursor);
4203 return cursor;
4204}
4205
4206#define STB_TEXTEDIT_MOVELINESTART STB_TEXTEDIT_MOVELINESTART_IMPL
4207#define STB_TEXTEDIT_MOVELINEEND STB_TEXTEDIT_MOVELINEEND_IMPL
4208
4209static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4210{
4211 // Offset remaining text (+ copy zero terminator)
4212 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4213 char* dst = obj->TextA.Data + pos;
4214 char* src = obj->TextA.Data + pos + n;
4215 memmove(dst, src, obj->TextLen - n - pos + 1);
4216 obj->EditedBefore = obj->EditedThisFrame = true;
4217 obj->TextLen -= n;
4218}
4219
4220static int STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4221{
4222 const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4223 const int text_len = obj->TextLen;
4224 IM_ASSERT(pos <= text_len);
4225
4226 // We support partial insertion (with a mod in stb_textedit.h)
4227 const int avail = obj->BufCapacity - 1 - obj->TextLen;
4228 if (!is_resizable && new_text_len > avail)
4229 new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4230 if (new_text_len == 0)
4231 return 0;
4232
4233 // Grow internal buffer if needed
4234 IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4235 if (text_len + new_text_len + 1 > obj->TextA.Size && is_resizable)
4236 {
4237 obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4238 obj->TextSrc = obj->TextA.Data;
4239 }
4240
4241 char* text = obj->TextA.Data;
4242 if (pos != text_len)
4243 memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4244 memcpy(text + pos, new_text, (size_t)new_text_len);
4245
4246 obj->EditedBefore = obj->EditedThisFrame = true;
4247 obj->TextLen += new_text_len;
4248 obj->TextA[obj->TextLen] = '\0';
4249
4250 return new_text_len;
4251}
4252
4253// 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)
4254#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4255#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4256#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4257#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4258#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4259#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4260#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4261#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4262#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4263#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4264#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4265#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4266#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4267#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4268#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4269#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4270#define STB_TEXTEDIT_K_SHIFT 0x400000
4271
4272#define IMSTB_TEXTEDIT_IMPLEMENTATION
4273#define IMSTB_TEXTEDIT_memmove memmove
4274#include "imstb_textedit.h"
4275
4276// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4277// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4278static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4279{
4280 stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4281 ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4282 state->cursor = state->select_start = state->select_end = 0;
4283 if (text_len <= 0)
4284 return;
4285 int text_len_inserted = ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len);
4286 if (text_len_inserted > 0)
4287 {
4288 state->cursor = state->select_start = state->select_end = text_len;
4289 state->has_preferred_x = 0;
4290 return;
4291 }
4292 IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4293}
4294
4295} // namespace ImStb
4296
4297// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4298ImGuiInputTextState::ImGuiInputTextState()
4299{
4300 memset((void*)this, 0, sizeof(*this));
4301 Stb = IM_NEW(ImStbTexteditState);
4302 memset(Stb, 0, sizeof(*Stb));
4303}
4304
4305ImGuiInputTextState::~ImGuiInputTextState()
4306{
4307 IM_DELETE(Stb);
4308}
4309
4310void ImGuiInputTextState::OnKeyPressed(int key)
4311{
4312 stb_textedit_key(this, Stb, key);
4313 CursorFollow = true;
4314 CursorAnimReset();
4315 const int key_u = (key & ~STB_TEXTEDIT_K_SHIFT);
4316 if (key_u == STB_TEXTEDIT_K_LEFT || key_u == STB_TEXTEDIT_K_LINESTART || key_u == STB_TEXTEDIT_K_TEXTSTART || key_u == STB_TEXTEDIT_K_BACKSPACE || key_u == STB_TEXTEDIT_K_WORDLEFT)
4317 LastMoveDirectionLR = ImGuiDir_Left;
4318 else if (key_u == STB_TEXTEDIT_K_RIGHT || key_u == STB_TEXTEDIT_K_LINEEND || key_u == STB_TEXTEDIT_K_TEXTEND || key_u == STB_TEXTEDIT_K_DELETE || key_u == STB_TEXTEDIT_K_WORDRIGHT)
4319 LastMoveDirectionLR = ImGuiDir_Right;
4320}
4321
4322void ImGuiInputTextState::OnCharPressed(unsigned int c)
4323{
4324 // Convert the key to a UTF8 byte sequence.
4325 // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4326 char utf8[5];
4327 ImTextCharToUtf8(utf8, c);
4328 stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8));
4329 CursorFollow = true;
4330 CursorAnimReset();
4331}
4332
4333// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4334void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4335void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); }
4336bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4337void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4338int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4339int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4340int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4341void ImGuiInputTextState::SetSelection(int start, int end) { Stb->select_start = start; Stb->cursor = Stb->select_end = end; }
4342float ImGuiInputTextState::GetPreferredOffsetX() const { return Stb->has_preferred_x ? Stb->preferred_x : -1; }
4343void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4344void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4345void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4346void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4347
4348ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4349{
4350 memset((void*)this, 0, sizeof(*this));
4351}
4352
4353// Public API to manipulate UTF-8 text from within a callback.
4354// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4355// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4356// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4357void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4358{
4359 IM_ASSERT(pos + bytes_count <= BufTextLen);
4360 char* dst = Buf + pos;
4361 const char* src = Buf + pos + bytes_count;
4362 memmove(dst, src, BufTextLen - bytes_count - pos + 1);
4363
4364 if (CursorPos >= pos + bytes_count)
4365 CursorPos -= bytes_count;
4366 else if (CursorPos >= pos)
4367 CursorPos = pos;
4368 SelectionStart = SelectionEnd = CursorPos;
4369 BufDirty = true;
4370 BufTextLen -= bytes_count;
4371}
4372
4373void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4374{
4375 // Accept null ranges
4376 if (new_text == new_text_end)
4377 return;
4378
4379 ImGuiContext& g = *Ctx;
4380 ImGuiInputTextState* obj = &g.InputTextState;
4381 IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
4382 const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4383 const bool is_readonly = (Flags & ImGuiInputTextFlags_ReadOnly) != 0;
4384 int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text);
4385
4386 // We support partial insertion (with a mod in stb_textedit.h)
4387 const int avail = BufSize - 1 - BufTextLen;
4388 if (!is_resizable && new_text_len > avail)
4389 new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4390 if (new_text_len == 0)
4391 return;
4392
4393 // Grow internal buffer if needed
4394 if (new_text_len + BufTextLen + 1 > obj->TextA.Size && is_resizable && !is_readonly)
4395 {
4396 IM_ASSERT(Buf == obj->TextA.Data);
4397 int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4398 obj->TextA.resize(new_buf_size + 1);
4399 obj->TextSrc = obj->TextA.Data;
4400 Buf = obj->TextA.Data;
4401 BufSize = obj->BufCapacity = new_buf_size;
4402 }
4403
4404 if (BufTextLen != pos)
4405 memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4406 memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4407 Buf[BufTextLen + new_text_len] = '\0';
4408
4409 BufDirty = true;
4410 BufTextLen += new_text_len;
4411 if (CursorPos >= pos)
4412 CursorPos += new_text_len;
4413 CursorPos = ImClamp(CursorPos, 0, BufTextLen);
4414 SelectionStart = SelectionEnd = CursorPos;
4415}
4416
4417void ImGui::PushPasswordFont()
4418{
4419 ImGuiContext& g = *GImGui;
4420 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4421 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4422 ImFontGlyph* glyph = g.FontBaked->FindGlyph('*');
4423 g.InputTextPasswordFontBackupFlags = g.Font->Flags;
4424 backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
4425 backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
4426 backup->IndexLookup.swap(g.FontBaked->IndexLookup);
4427 backup->IndexAdvanceX.swap(g.FontBaked->IndexAdvanceX);
4428 g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
4429 g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(glyph);
4430 g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
4431}
4432
4433void ImGui::PopPasswordFont()
4434{
4435 ImGuiContext& g = *GImGui;
4436 ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4437 g.Font->Flags = g.InputTextPasswordFontBackupFlags;
4438 g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
4439 g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
4440 g.FontBaked->IndexLookup.swap(backup->IndexLookup);
4441 g.FontBaked->IndexAdvanceX.swap(backup->IndexAdvanceX);
4442 IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4443}
4444
4445// Return false to discard a character.
4446static bool InputTextFilterCharacter(ImGuiContext* ctx, ImGuiInputTextState* state, unsigned int* p_char, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4447{
4448 IM_ASSERT(state != NULL);
4449 unsigned int c = *p_char;
4450 ImGuiInputTextFlags flags = state->Flags;
4451
4452 // Filter non-printable (NB: isprint is unreliable! see #2467)
4453 bool apply_named_filters = true;
4454 if (c < 0x20)
4455 {
4456 bool pass = false;
4457 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code)
4458 if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4459 {
4460 c = *p_char = ' ';
4461 pass = true;
4462 }
4463 pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4464 pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4465 if (!pass)
4466 return false;
4467 apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4468 }
4469
4470 if (input_source_is_clipboard == false)
4471 {
4472 // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4473 if (c == 127)
4474 return false;
4475
4476 // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4477 if (c >= 0xE000 && c <= 0xF8FF)
4478 return false;
4479 }
4480
4481 // Filter Unicode ranges we are not handling in this build
4482 if (c > IM_UNICODE_CODEPOINT_MAX)
4483 return false;
4484
4485 // Generic named filters
4486 if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4487 {
4488 // 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 '.'.
4489 // The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4490 // 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.
4491 // Change the default decimal_point with:
4492 // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4493 // 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.
4494 ImGuiContext& g = *ctx;
4495 const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4496 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4497 if (c == '.' || c == ',')
4498 c = c_decimal_point;
4499
4500 // Full-width -> half-width conversion for numeric fields: https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4501 // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4502 // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4503 if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4504 if (c >= 0xFF01 && c <= 0xFF5E)
4505 c = c - 0xFF01 + 0x21;
4506
4507 // Allow 0-9 . - + * /
4508 if (flags & ImGuiInputTextFlags_CharsDecimal)
4509 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4510 return false;
4511
4512 // Allow 0-9 . - + * / e E
4513 if (flags & ImGuiInputTextFlags_CharsScientific)
4514 if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4515 return false;
4516
4517 // Allow 0-9 a-F A-F
4518 if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4519 if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4520 return false;
4521
4522 // Turn a-z into A-Z
4523 if (flags & ImGuiInputTextFlags_CharsUppercase)
4524 if (c >= 'a' && c <= 'z')
4525 c += (unsigned int)('A' - 'a');
4526
4527 if (flags & ImGuiInputTextFlags_CharsNoBlank)
4528 if (ImCharIsBlankW(c))
4529 return false;
4530
4531 *p_char = c;
4532 }
4533
4534 // Custom callback filter
4535 if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4536 {
4537 ImGuiContext& g = *GImGui;
4538 ImGuiInputTextCallbackData callback_data;
4539 callback_data.Ctx = &g;
4540 callback_data.ID = state->ID;
4541 callback_data.Flags = flags;
4542 callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4543 callback_data.EventChar = (ImWchar)c;
4544 callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
4545 callback_data.CursorPos = state->Stb->cursor;
4546 callback_data.SelectionStart = state->Stb->select_start;
4547 callback_data.SelectionEnd = state->Stb->select_end;
4548 callback_data.UserData = user_data;
4549 if (callback(&callback_data) != 0)
4550 return false;
4551 *p_char = callback_data.EventChar;
4552 if (!callback_data.EventChar)
4553 return false;
4554 }
4555
4556 return true;
4557}
4558
4559// Find the shortest single replacement we can make to get from old_buf to new_buf
4560// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4561// 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.
4562static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4563{
4564 const int shorter_length = ImMin(old_length, new_length);
4565 int first_diff;
4566 for (first_diff = 0; first_diff < shorter_length; first_diff++)
4567 if (old_buf[first_diff] != new_buf[first_diff])
4568 break;
4569 if (first_diff == old_length && first_diff == new_length)
4570 return;
4571
4572 int old_last_diff = old_length - 1;
4573 int new_last_diff = new_length - 1;
4574 for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4575 if (old_buf[old_last_diff] != new_buf[new_last_diff])
4576 break;
4577
4578 const int insert_len = new_last_diff - first_diff + 1;
4579 const int delete_len = old_last_diff - first_diff + 1;
4580 if (insert_len > 0 || delete_len > 0)
4581 if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4582 for (int i = 0; i < delete_len; i++)
4583 p[i] = old_buf[first_diff + i];
4584}
4585
4586// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4587// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4588// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4589// but that more likely be attractive when we do have _NoLiveEdit flag available.
4590void ImGui::InputTextDeactivateHook(ImGuiID id)
4591{
4592 ImGuiContext& g = *GImGui;
4593 ImGuiInputTextState* state = &g.InputTextState;
4594 if (id == 0 || state->ID != id)
4595 return;
4596 //IMGUI_DEBUG_LOG_ACTIVEID("InputTextDeactivateHook() id = 0x%08X\n", id);
4597 g.InputTextDeactivatedState.ID = state->ID;
4598 if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4599 {
4600 g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4601 }
4602 else
4603 {
4604 IM_ASSERT(state->TextA.Data != 0);
4605 IM_ASSERT(state->TextA[state->TextLen] == 0);
4606 g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4607 memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4608 }
4609}
4610
4611static int* ImLowerBound(int* in_begin, int* in_end, int v)
4612{
4613 int* in_p = in_begin;
4614 for (size_t count = (size_t)(in_end - in_p); count > 0; )
4615 {
4616 size_t count2 = count >> 1;
4617 int* mid = in_p + count2;
4618 if (*mid < v)
4619 {
4620 in_p = ++mid;
4621 count -= count2 + 1;
4622 }
4623 else
4624 {
4625 count = count2;
4626 }
4627 }
4628 return in_p;
4629}
4630
4631// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool?
4632// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive)
4633static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size, const char** out_buf_end)
4634{
4635 ImGuiContext& g = *GImGui;
4636 int size = 0;
4637 const char* s;
4638 bool trailing_line_already_counted = false;
4639 if (flags & ImGuiInputTextFlags_WordWrap)
4640 {
4641 for (s = buf; s < buf_end; s = (*s == '\n') ? s + 1 : s)
4642 {
4643 if (size++ <= max_output_buffer_size)
4644 line_index->Offsets.push_back((int)(s - buf));
4645 s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
4646 }
4647 }
4648 else if (buf_end != NULL)
4649 {
4650 for (s = buf; s < buf_end; s = s ? s + 1 : buf_end)
4651 {
4652 if (size++ <= max_output_buffer_size)
4653 line_index->Offsets.push_back((int)(s - buf));
4654 s = (const char*)ImMemchr(s, '\n', buf_end - s);
4655 }
4656 }
4657 else
4658 {
4659 // Inactive path: we don't know buf_end ahead of time.
4660 const char* s_eol;
4661 for (s = buf; ; s = s_eol + 1)
4662 {
4663 if (size++ <= max_output_buffer_size)
4664 line_index->Offsets.push_back((int)(s - buf));
4665 if ((s_eol = strchr(s, '\n')) != NULL)
4666 continue;
4667 s += strlen(s);
4668 trailing_line_already_counted = true;
4669 break;
4670 }
4671 }
4672 if (out_buf_end != NULL)
4673 *out_buf_end = buf_end = s;
4674 if (size == 0)
4675 {
4676 line_index->Offsets.push_back(0);
4677 size++;
4678 }
4679 if (buf_end > buf && buf_end[-1] == '\n' && !trailing_line_already_counted && size++ <= max_output_buffer_size)
4680 line_index->Offsets.push_back((int)(buf_end - buf));
4681 return size;
4682}
4683
4684static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n)
4685{
4686 const char* cursor_ptr = buf + cursor_n;
4687 int* it_begin = line_index->Offsets.begin();
4688 int* it_end = line_index->Offsets.end();
4689 const int* it = ImLowerBound(it_begin, it_end, cursor_n);
4690 if (it > it_begin)
4691 if (it == it_end || *it != cursor_n || (state != NULL && state->WrapWidth > 0.0f && state->LastMoveDirectionLR == ImGuiDir_Right && cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0))
4692 it--;
4693
4694 const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it);
4695 const char* line_start = line_index->get_line_begin(buf, line_no);
4696 ImVec2 offset;
4697 offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
4698 offset.y = (line_no + 1) * g.FontSize;
4699 return offset;
4700}
4701
4702// Edit a string of text
4703// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4704// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4705// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4706// - 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.
4707// - If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
4708bool 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)
4709{
4710 ImGuiWindow* window = GetCurrentWindow();
4711 if (window->SkipItems)
4712 return false;
4713
4714 IM_ASSERT(buf != NULL && buf_size >= 0);
4715 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4716 IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4717 IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline does not not work with left-trimming
4718 IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Password) == 0); // WordWrap does not work with Password mode.
4719 IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Multiline) != 0); // WordWrap does not work in single-line mode.
4720
4721 ImGuiContext& g = *GImGui;
4722 ImGuiIO& io = g.IO;
4723 const ImGuiStyle& style = g.Style;
4724
4725 const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4726 const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4727
4728 if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4729 BeginGroup();
4730 const ImGuiID id = window->GetID(label);
4731 const char* label_end = FindRenderedTextEnd(label);
4732 const ImVec2 label_size = CalcTextSize(label, label_end, false);
4733 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line
4734 const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4735
4736 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4737 const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4738
4739 ImGuiWindow* draw_window = window;
4740 ImVec2 inner_size = frame_size;
4741 ImGuiLastItemData item_data_backup;
4742 if (is_multiline)
4743 {
4744 ImVec2 backup_pos = window->DC.CursorPos;
4745 ItemSize(total_bb, style.FramePadding.y);
4746 bool no_clip = (g.InputTextDeactivatedState.ID == id) || (g.ActiveId == id) || (id == g.NavActivateId); // Mimic some of ItemAdd() logic + add InputTextDeactivatedState.ID check.
4747 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable) && !no_clip)
4748 {
4749 EndGroup();
4750 return false;
4751 }
4752 item_data_backup = g.LastItemData;
4753 window->DC.CursorPos = backup_pos;
4754
4755 // Prevent NavActivation from explicit Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4756 if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && !(g.NavActivateFlags & ImGuiActivateFlags_FromFocusApi) && (flags & ImGuiInputTextFlags_AllowTabInput))
4757 g.NavActivateId = 0;
4758
4759 // Prevent NavActivate reactivating in BeginChild() when we are already active.
4760 const ImGuiID backup_activate_id = g.NavActivateId;
4761 if (g.ActiveId == id) // Prevent reactivation
4762 g.NavActivateId = 0;
4763
4764 // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4765 PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4766 PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4767 PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4768 PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4769 bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4770 g.NavActivateId = backup_activate_id;
4771 PopStyleVar(3);
4772 PopStyleColor();
4773 if (!child_visible && !no_clip)
4774 {
4775 EndChild();
4776 EndGroup();
4777 return false;
4778 }
4779 draw_window = g.CurrentWindow; // Child window
4780 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.
4781 draw_window->DC.CursorPos += style.FramePadding;
4782 inner_size.x -= draw_window->ScrollbarSizes.x;
4783
4784 // FIXME: Could this be a ImGuiChildFlags to affect the SetLastItemDataForWindow() call?
4785 g.LastItemData.ID = id;
4786 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
4787 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
4788 }
4789 else
4790 {
4791 // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4792 ItemSize(total_bb, style.FramePadding.y);
4793 if (!(flags & ImGuiInputTextFlags_TempInput))
4794 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4795 return false;
4796 }
4797
4798 // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4799 bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4800 if (hovered)
4801 SetMouseCursor(ImGuiMouseCursor_TextInput);
4802 if (hovered && g.NavHighlightItemUnderNav)
4803 hovered = false;
4804
4805 // We are only allowed to access the state if we are already the active widget.
4806 ImGuiInputTextState* state = GetInputTextState(id);
4807
4808 if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4809 flags |= ImGuiInputTextFlags_ReadOnly;
4810 const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4811 const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4812 const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4813 const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4814 if (is_resizable)
4815 IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4816
4817 // Word-wrapping: enforcing a fixed width not altered by vertical scrollbar makes things easier, notably to track cursor reliably and avoid one-frame glitches.
4818 // Instead of using ImGuiWindowFlags_AlwaysVerticalScrollbar we account for that space if the scrollbar is not visible.
4819 const bool is_wordwrap = (flags & ImGuiInputTextFlags_WordWrap) != 0;
4820 float wrap_width = 0.0f;
4821 if (is_wordwrap)
4822 wrap_width = ImMax(1.0f, GetContentRegionAvail().x + (draw_window->ScrollbarY ? 0.0f : -g.Style.ScrollbarSize));
4823
4824 const bool user_clicked = hovered && io.MouseClicked[0];
4825 const bool input_requested_by_nav = (g.ActiveId != id) && (g.NavActivateId == id);
4826 const bool input_requested_by_reactivate = (g.InputTextReactivateId == id); // for io.ConfigInputTextEnterKeepActive
4827 const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput));
4828 const ImGuiID scrollbar_id = (is_multiline && state != NULL) ? GetWindowScrollbarID(draw_window, ImGuiAxis_Y) : 0;
4829 const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == scrollbar_id;
4830 const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == scrollbar_id;
4831 bool clear_active_id = false;
4832 bool select_all = false;
4833
4834 float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4835
4836 const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4837 const bool init_changed_specs_multiline = (state != NULL && (state->Stb->single_line != !is_multiline)); // state != NULL means its our state.
4838 const bool init_changed_specs_readonly = (state != NULL && ((state->Flags ^ flags) & ImGuiInputTextFlags_ReadOnly)); // state != NULL means its our state.
4839 const bool init_make_active = (input_requested_by_user || input_requested_by_nav || input_requested_by_reactivate || user_scroll_finish);
4840 if (init_reload_from_user_buf)
4841 {
4842 int new_len = (int)ImStrlen(buf);
4843 IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4844 state->WantReloadUserBuf = false;
4845 InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len);
4846 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4847 state->TextLen = new_len;
4848 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4849 state->Stb->select_start = state->ReloadSelectionStart;
4850 state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; // will be clamped to bounds below
4851 }
4852 else if ((init_make_active && g.ActiveId != id) || init_changed_specs_multiline || init_changed_specs_readonly)
4853 {
4854 // Access state even if we don't own it yet.
4855 state = &g.InputTextState;
4856 state->CursorAnimReset();
4857
4858 // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714)
4859 InputTextDeactivateHook(state->ID);
4860
4861 // Take a copy of the initial buffer value.
4862 // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4863 const int buf_len = (int)ImStrlen(buf);
4864 IM_ASSERT(((buf_len + 1 <= buf_size) || (buf_len == 0 && buf_size == 0)) && "Is your input buffer properly zero-terminated?");
4865 if (!user_scroll_finish)
4866 {
4867 state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string.
4868 memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4869 }
4870
4871 // Preserve cursor position and undo/redo stack if we come back to same widget
4872 // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4873 bool recycle_state = (state->ID == id && !init_changed_specs_multiline);
4874 if (recycle_state && !init_changed_specs_readonly && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
4875 recycle_state = false;
4876
4877 // Start edition
4878 state->ID = id;
4879 state->TextLen = buf_len;
4880 state->EditedBefore = false;
4881 if (!is_readonly)
4882 {
4883 state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4884 memcpy(state->TextA.Data, buf, state->TextLen + 1);
4885 }
4886
4887 // Find initial scroll position for right alignment
4888 state->Scroll = ImVec2(0.0f, 0.0f);
4889 if (flags & ImGuiInputTextFlags_ElideLeft)
4890 state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4891
4892 // Recycle existing cursor/selection/undo stack but clamp position
4893 // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4894 if (!recycle_state)
4895 stb_textedit_initialize_state(state->Stb, !is_multiline);
4896
4897 if (!is_multiline)
4898 {
4899 if (flags & ImGuiInputTextFlags_AutoSelectAll)
4900 select_all = true;
4901 if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4902 select_all = true;
4903 if (user_clicked && io.KeyCtrl)
4904 select_all = true;
4905 }
4906
4907 if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4908 state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4909 }
4910
4911 const bool is_osx = io.ConfigMacOSXBehaviors;
4912 if (init_make_active && g.ActiveId != id)
4913 {
4914 IM_ASSERT(state && state->ID == id);
4915 SetActiveID(id, window);
4916 SetFocusID(id, window);
4917 FocusWindow(window);
4918 if (input_requested_by_nav)
4919 SetNavCursorVisibleAfterMove();
4920 }
4921 if (g.ActiveId == id)
4922 {
4923 // Declare some inputs, the other are registered and polled via Shortcut() routing system.
4924 // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combination into individual shortcuts.
4925 const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4926 for (ImGuiKey key : always_owned_keys)
4927 SetKeyOwner(key, id);
4928 if (user_clicked)
4929 SetKeyOwner(ImGuiKey_MouseLeft, id);
4930 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4931 if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4932 {
4933 g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4934 SetKeyOwner(ImGuiKey_UpArrow, id);
4935 SetKeyOwner(ImGuiKey_DownArrow, id);
4936 }
4937 if (is_multiline)
4938 {
4939 SetKeyOwner(ImGuiKey_PageUp, id);
4940 SetKeyOwner(ImGuiKey_PageDown, id);
4941 }
4942 // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4943 if (is_osx)
4944 SetKeyOwner(ImGuiMod_Alt, id);
4945
4946 // Expose scroll in a manner that is agnostic to us using a child window
4947 if (is_multiline && state != NULL)
4948 state->Scroll.y = draw_window->Scroll.y;
4949
4950 // Read-only mode always ever read from source buffer. Refresh TextLen when active.
4951 if (is_readonly && state != NULL)
4952 state->TextLen = (int)ImStrlen(buf);
4953 if (state != NULL)
4954 state->CursorClamp();
4955 //if (is_readonly && state != NULL)
4956 // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4957 }
4958 if (state != NULL)
4959 state->TextSrc = is_readonly ? buf : state->TextA.Data;
4960
4961 // 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)
4962 if (g.ActiveId == id && state == NULL)
4963 ClearActiveID();
4964
4965 // Release focus when we click outside
4966 if (g.ActiveId == id && io.MouseClicked[0] && !init_make_active) //-V560
4967 clear_active_id = true;
4968
4969 // Lock the decision of whether we are going to take the path displaying the cursor or selection
4970 bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4971 bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4972 bool value_changed = false;
4973 bool validated = false;
4974
4975 // Select the buffer to render.
4976 const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4977 bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4978
4979 // Password pushes a temporary font with only a fallback glyph
4980 if (is_password && !is_displaying_hint)
4981 PushPasswordFont();
4982
4983 if (state != NULL && state->ID == id)
4984 {
4985 state->Flags = flags;
4986
4987 // Word-wrapping: attempt to keep cursor in view while resizing frame/parent (FIXME-WORDWRAP: would be better to preserve same relative offset)
4988 if (is_wordwrap && state->WrapWidth != wrap_width)
4989 {
4990 state->CursorCenterY = true;
4991 state->WrapWidth = wrap_width;
4992 render_cursor = true;
4993 }
4994 }
4995
4996 // Process mouse inputs and character inputs
4997 if (g.ActiveId == id)
4998 {
4999 IM_ASSERT(state != NULL);
5000 state->EditedThisFrame = false;
5001 state->BufCapacity = buf_size;
5002 state->WrapWidth = wrap_width;
5003
5004 // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
5005 // Down the line we should have a cleaner library-wide concept of Selected vs Active.
5006 g.ActiveIdAllowOverlap = !io.MouseDown[0];
5007
5008 // Edit in progress
5009 const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
5010 const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
5011
5012 if (select_all)
5013 {
5014 state->SelectAll();
5015 state->SelectedAllMouseLock = true;
5016 }
5017 else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
5018 {
5019 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
5020 const int multiclick_count = (io.MouseClickedCount[0] - 2);
5021 if ((multiclick_count % 2) == 0)
5022 {
5023 // Double-click: Select word
5024 // We always use the "Mac" word advance for double-click select vs Ctrl+Right which use the platform dependent variant:
5025 // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
5026 const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
5027 if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
5028 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
5029 //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
5030 if (!STB_TEXT_HAS_SELECTION(state->Stb))
5031 ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
5032 state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
5033 state->Stb->select_end = state->Stb->cursor;
5034 ImStb::stb_textedit_clamp(state, state->Stb);
5035 }
5036 else
5037 {
5038 // Triple-click: Select line
5039 const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
5040 state->WrapWidth = 0.0f; // Temporarily disable wrapping so we use real line start.
5041 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
5042 state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
5043 state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
5044 state->WrapWidth = wrap_width;
5045 if (!is_eol && is_multiline)
5046 {
5047 ImSwap(state->Stb->select_start, state->Stb->select_end);
5048 state->Stb->cursor = state->Stb->select_end;
5049 }
5050 state->CursorFollow = false;
5051 }
5052 state->CursorAnimReset();
5053 }
5054 else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
5055 {
5056 if (hovered)
5057 {
5058 if (io.KeyShift)
5059 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
5060 else
5061 stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
5062 state->CursorAnimReset();
5063 }
5064 }
5065 else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
5066 {
5067 stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
5068 state->CursorAnimReset();
5069 state->CursorFollow = true;
5070 }
5071 if (state->SelectedAllMouseLock && !io.MouseDown[0])
5072 state->SelectedAllMouseLock = false;
5073
5074 // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
5075 // (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)
5076 if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
5077 {
5078 if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
5079 {
5080 unsigned int c = '\t'; // Insert TAB
5081 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5082 state->OnCharPressed(c);
5083 }
5084 // FIXME: Implement Shift+Tab
5085 /*
5086 if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
5087 {
5088 }
5089 */
5090 }
5091
5092 // Process regular text input (before we check for Return because using some IME will effectively send a Return?)
5093 // 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.
5094 const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
5095 if (io.InputQueueCharacters.Size > 0)
5096 {
5097 if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
5098 for (int n = 0; n < io.InputQueueCharacters.Size; n++)
5099 {
5100 // Insert character if they pass filtering
5101 unsigned int c = (unsigned int)io.InputQueueCharacters[n];
5102 if (c == '\t') // Skip Tab, see above.
5103 continue;
5104 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5105 state->OnCharPressed(c);
5106 }
5107
5108 // Consume characters
5109 io.InputQueueCharacters.resize(0);
5110 }
5111 }
5112
5113 // Process other shortcuts/key-presses
5114 bool revert_edit = false;
5115 if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
5116 {
5117 IM_ASSERT(state != NULL);
5118
5119 const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
5120 state->Stb->row_count_per_page = row_count_per_page;
5121
5122 const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
5123 const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
5124 const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End
5125
5126 // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use Ctrl+A and Ctrl+B: former would be handled by InputText)
5127 // Otherwise we could simply assume that we own the keys as we are active.
5128 const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
5129 const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection());
5130 const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
5131 const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
5132 const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
5133 const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
5134 const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
5135
5136 // 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.
5137 const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
5138 const bool is_enter = Shortcut(ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiKey_KeypadEnter, f_repeat, id);
5139 const bool is_ctrl_enter = Shortcut(ImGuiMod_Ctrl | ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_KeypadEnter, f_repeat, id);
5140 const bool is_shift_enter = Shortcut(ImGuiMod_Shift | ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_KeypadEnter, f_repeat, id);
5141 const bool is_gamepad_validate = nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, false);
5142 const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
5143
5144 // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
5145 // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
5146 if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); }
5147 else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); }
5148 else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); }
5149 else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); }
5150 else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
5151 else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
5152 else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
5153 else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
5154 else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
5155 {
5156 if (!state->HasSelection())
5157 {
5158 // OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace)
5159 if (is_wordmove_key_down)
5160 state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
5161 }
5162 state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
5163 }
5164 else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
5165 {
5166 if (!state->HasSelection())
5167 {
5168 if (is_wordmove_key_down)
5169 state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
5170 else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
5171 state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
5172 }
5173 state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
5174 }
5175 else if (is_enter || is_ctrl_enter || is_shift_enter || is_gamepad_validate)
5176 {
5177 // Determine if we turn Enter into a \n character
5178 bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
5179 bool is_new_line = is_multiline && !is_gamepad_validate && (is_shift_enter || (is_enter && !ctrl_enter_for_new_line) || (is_ctrl_enter && ctrl_enter_for_new_line));
5180 if (!is_new_line)
5181 {
5182 validated = clear_active_id = true;
5183 if (io.ConfigInputTextEnterKeepActive && !is_multiline)
5184 {
5185 // Queue reactivation, so that e.g. IsItemDeactivatedAfterEdit() will work. (#9001)
5186 state->SelectAll(); // No need to scroll
5187 g.InputTextReactivateId = id; // Mark for reactivation on next frame
5188 }
5189 }
5190 else if (!is_readonly)
5191 {
5192 // Insert new line
5193 unsigned int c = '\n';
5194 if (InputTextFilterCharacter(&g, state, &c, callback, callback_user_data))
5195 state->OnCharPressed(c);
5196 }
5197 }
5198 else if (is_cancel)
5199 {
5200 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5201 {
5202 if (state->TextA.Data[0] != 0)
5203 {
5204 revert_edit = true;
5205 }
5206 else
5207 {
5208 render_cursor = render_selection = false;
5209 clear_active_id = true;
5210 }
5211 }
5212 else
5213 {
5214 clear_active_id = revert_edit = true;
5215 render_cursor = render_selection = false;
5216 }
5217 }
5218 else if (is_undo || is_redo)
5219 {
5220 state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
5221 state->ClearSelection();
5222 }
5223 else if (is_select_all)
5224 {
5225 state->SelectAll();
5226 state->CursorFollow = true;
5227 }
5228 else if (is_cut || is_copy)
5229 {
5230 // Cut, Copy
5231 if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
5232 {
5233 // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
5234 const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
5235 const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
5236 g.TempBuffer.reserve(ie - ib + 1);
5237 memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib);
5238 g.TempBuffer.Data[ie - ib] = 0;
5239 SetClipboardText(g.TempBuffer.Data);
5240 }
5241 if (is_cut)
5242 {
5243 if (!state->HasSelection())
5244 state->SelectAll();
5245 state->CursorFollow = true;
5246 stb_textedit_cut(state, state->Stb);
5247 }
5248 }
5249 else if (is_paste)
5250 {
5251 if (const char* clipboard = GetClipboardText())
5252 {
5253 // Filter pasted buffer
5254 const int clipboard_len = (int)ImStrlen(clipboard);
5255 const char* clipboard_end = clipboard + clipboard_len;
5256 ImVector<char> clipboard_filtered;
5257 clipboard_filtered.reserve(clipboard_len + 1);
5258 for (const char* s = clipboard; *s != 0; )
5259 {
5260 unsigned int c;
5261 int in_len = ImTextCharFromUtf8(&c, s, clipboard_end);
5262 s += in_len;
5263 if (!InputTextFilterCharacter(&g, state, &c, callback, callback_user_data, true))
5264 continue;
5265 char c_utf8[5];
5266 ImTextCharToUtf8(c_utf8, c);
5267 int out_len = (int)ImStrlen(c_utf8);
5268 clipboard_filtered.resize(clipboard_filtered.Size + out_len);
5269 memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len);
5270 }
5271 if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
5272 {
5273 clipboard_filtered.push_back(0);
5274 stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1);
5275 state->CursorFollow = true;
5276 }
5277 }
5278 }
5279
5280 // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
5281 render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
5282 }
5283
5284 // Process revert and user callbacks
5285 const char* apply_new_text = NULL;
5286 int apply_new_text_length = 0;
5287 if (g.ActiveId == id)
5288 {
5289 IM_ASSERT(state != NULL);
5290 if (revert_edit && !is_readonly)
5291 {
5292 if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5293 {
5294 // Clear input
5295 IM_ASSERT(state->TextA.Data[0] != 0);
5296 apply_new_text = "";
5297 apply_new_text_length = 0;
5298 value_changed = true;
5299 char empty_string = 0;
5300 stb_textedit_replace(state, state->Stb, &empty_string, 0);
5301 }
5302 else if (strcmp(state->TextA.Data, state->TextToRevertTo.Data) != 0)
5303 {
5304 apply_new_text = state->TextToRevertTo.Data;
5305 apply_new_text_length = state->TextToRevertTo.Size - 1;
5306
5307 // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5308 // Push records into the undo stack so we can Ctrl+Z the revert operation itself
5309 value_changed = true;
5310 stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
5311 }
5312 }
5313
5314 // User callback
5315 if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5316 {
5317 IM_ASSERT(callback != NULL);
5318
5319 // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5320 ImGuiInputTextFlags event_flag = 0;
5321 ImGuiKey event_key = ImGuiKey_None;
5322 if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
5323 {
5324 event_flag = ImGuiInputTextFlags_CallbackCompletion;
5325 event_key = ImGuiKey_Tab;
5326 }
5327 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
5328 {
5329 event_flag = ImGuiInputTextFlags_CallbackHistory;
5330 event_key = ImGuiKey_UpArrow;
5331 }
5332 else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
5333 {
5334 event_flag = ImGuiInputTextFlags_CallbackHistory;
5335 event_key = ImGuiKey_DownArrow;
5336 }
5337 else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->EditedThisFrame)
5338 {
5339 event_flag = ImGuiInputTextFlags_CallbackEdit;
5340 }
5341 else if (flags & ImGuiInputTextFlags_CallbackAlways)
5342 {
5343 event_flag = ImGuiInputTextFlags_CallbackAlways;
5344 }
5345
5346 if (event_flag)
5347 {
5348 ImGuiInputTextCallbackData callback_data;
5349 callback_data.Ctx = &g;
5350 callback_data.ID = id;
5351 callback_data.Flags = flags;
5352 callback_data.EventFlag = event_flag;
5353 callback_data.EventActivated = (g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
5354 callback_data.UserData = callback_user_data;
5355
5356 // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5357 char* callback_buf = is_readonly ? buf : state->TextA.Data;
5358 IM_ASSERT(callback_buf == state->TextSrc);
5359 state->CallbackTextBackup.resize(state->TextLen + 1);
5360 memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);
5361
5362 callback_data.EventKey = event_key;
5363 callback_data.Buf = callback_buf;
5364 callback_data.BufTextLen = state->TextLen;
5365 callback_data.BufSize = state->BufCapacity;
5366 callback_data.BufDirty = false;
5367 callback_data.CursorPos = state->Stb->cursor;
5368 callback_data.SelectionStart = state->Stb->select_start;
5369 callback_data.SelectionEnd = state->Stb->select_end;
5370
5371 // Call user code
5372 callback(&callback_data);
5373
5374 // Read back what user may have modified
5375 callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5376 IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5377 IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5378 IM_ASSERT(callback_data.Flags == flags);
5379 if (callback_data.BufDirty || callback_data.CursorPos != state->Stb->cursor)
5380 state->CursorFollow = true;
5381 state->Stb->cursor = ImClamp(callback_data.CursorPos, 0, callback_data.BufTextLen);
5382 state->Stb->select_start = ImClamp(callback_data.SelectionStart, 0, callback_data.BufTextLen);
5383 state->Stb->select_end = ImClamp(callback_data.SelectionEnd, 0, callback_data.BufTextLen);
5384 if (callback_data.BufDirty)
5385 {
5386 // Callback may update buffer and thus set buf_dirty even in read-only mode.
5387 IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5388 InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen);
5389 state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5390 state->CursorAnimReset();
5391 }
5392 }
5393 }
5394
5395 // Will copy result string if modified.
5396 // FIXME-OPT: Could mark dirty state from the stb_textedit callbacks
5397 if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
5398 {
5399 apply_new_text = state->TextSrc;
5400 apply_new_text_length = state->TextLen;
5401 value_changed = true;
5402 }
5403 }
5404
5405 // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5406 // This is used when e.g. losing focus or tabbing out into another InputText() which may already be using the temp buffer.
5407 if (g.InputTextDeactivatedState.ID == id)
5408 {
5409 if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5410 {
5411 apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5412 apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5413 value_changed = true;
5414 //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5415 }
5416 g.InputTextDeactivatedState.ID = 0;
5417 }
5418
5419 // Write back result to user buffer. This can currently only happen when (g.ActiveId == id) or when just deactivated.
5420 // - As soon as the InputText() is active, our stored in-widget value gets priority over any underlying modification of the user buffer.
5421 // - Make sure we always reapply the live buffer back to the input/user buffer before clearing ActiveId, even thought strictly speaking
5422 // it was not modified on this frame. This allows the user to use InputText() without maintaining any user-side storage.
5423 // (PS: if you use this property together with ImGuiInputTextFlags_CallbackResize, you are at the risk of recreating a temporary
5424 // allocated/string object every frame. Which in the grand scheme of scheme is nothing, but isn't dear imgui vibe).
5425 if (apply_new_text != NULL)
5426 {
5427 IM_ASSERT(apply_new_text_length >= 0);
5428 if (is_resizable)
5429 {
5430 ImGuiInputTextCallbackData callback_data;
5431 callback_data.Ctx = &g;
5432 callback_data.ID = id;
5433 callback_data.Flags = flags;
5434 callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5435 callback_data.EventActivated = (state != NULL && g.ActiveId == state->ID && g.ActiveIdIsJustActivated);
5436 callback_data.Buf = buf;
5437 callback_data.BufTextLen = apply_new_text_length;
5438 callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5439 callback_data.UserData = callback_user_data;
5440 callback(&callback_data);
5441 buf = callback_data.Buf;
5442 buf_size = callback_data.BufSize;
5443 apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5444 IM_ASSERT(apply_new_text_length <= buf_size);
5445 }
5446 //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5447
5448 // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5449 ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5450 }
5451
5452 // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5453 // Otherwise request text input ahead for next frame.
5454 if (g.ActiveId == id && clear_active_id)
5455 ClearActiveID();
5456
5457 // Render frame
5458 if (!is_multiline)
5459 {
5460 RenderNavCursor(frame_bb, id);
5461 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5462 }
5463
5464 ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5465 ImRect 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
5466 if (is_multiline)
5467 clip_rect.ClipWith(draw_window->ClipRect);
5468
5469 // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5470 // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5471 // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5472 const int buf_display_max_length = 2 * 1024 * 1024;
5473 const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5474 const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5475
5476 // Display hint when contents is empty
5477 // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5478 const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5479 if (new_is_displaying_hint != is_displaying_hint)
5480 {
5481 if (is_password && !is_displaying_hint)
5482 PopPasswordFont();
5483 is_displaying_hint = new_is_displaying_hint;
5484 if (is_password && !is_displaying_hint)
5485 PushPasswordFont();
5486 }
5487 if (is_displaying_hint)
5488 {
5489 buf_display = hint;
5490 buf_display_end = hint + ImStrlen(hint);
5491 }
5492 else
5493 {
5494 if (render_cursor || render_selection || g.ActiveId == id)
5495 buf_display_end = buf_display + state->TextLen; //-V595
5496 else if (is_multiline && !is_wordwrap)
5497 buf_display_end = NULL; // Inactive multi-line: end of buffer will be output by InputTextLineIndexBuild() special strchr() path.
5498 else
5499 buf_display_end = buf_display + ImStrlen(buf_display);
5500 }
5501
5502 // Calculate visibility
5503 int line_visible_n0 = 0, line_visible_n1 = 1;
5504 if (is_multiline)
5505 CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5506
5507 // Build line index for easy data access (makes code below simpler and faster)
5508 ImGuiTextIndex* line_index = &g.InputTextLineIndex;
5509 line_index->Offsets.resize(0);
5510 int line_count = 1;
5511 if (is_multiline)
5512 {
5513 // If scrolling is expected to change build full index.
5514 // FIXME-OPT: Could append to index when new value of line_visible_n1 becomes bigger, see second call to CalcClipRectVisibleItemsY() below.
5515 bool will_scroll_y = state && ((state->CursorFollow && render_cursor) || (state->CursorCenterY && (render_cursor || render_selection)));
5516 line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, will_scroll_y ? INT_MAX : line_visible_n1 + 1, buf_display_end ? NULL : &buf_display_end);
5517 }
5518 line_index->EndOffset = (int)(buf_display_end - buf_display);
5519 line_visible_n1 = ImMin(line_visible_n1, line_count);
5520
5521 // Store text height (we don't need width)
5522 float text_size_y = line_count * g.FontSize;
5523 //GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255));
5524
5525 // Calculate blinking cursor position
5526 const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f);
5527 ImVec2 draw_scroll;
5528
5529 // Render text. We currently only render selection when the widget is active or while scrolling.
5530 const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5531 if (render_cursor || render_selection)
5532 {
5533 // Render text (with cursor and selection)
5534 // This is going to be messy. We need to:
5535 // - Display the text (this alone can be more easily clipped)
5536 // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5537 // - Measure text height (for scrollbar)
5538 // 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)
5539 // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5540 IM_ASSERT(state != NULL);
5541 state->LineCount = line_count;
5542
5543 // Scroll
5544 float new_scroll_y = scroll_y;
5545 if (render_cursor && state->CursorFollow)
5546 {
5547 // Horizontal scroll in chunks of quarter width
5548 if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5549 {
5550 const float scroll_increment_x = inner_size.x * 0.25f;
5551 const float visible_width = inner_size.x - style.FramePadding.x;
5552 if (cursor_offset.x < state->Scroll.x)
5553 state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5554 else if (cursor_offset.x - visible_width >= state->Scroll.x)
5555 state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5556 }
5557 else
5558 {
5559 state->Scroll.x = 0.0f;
5560 }
5561
5562 // Vertical scroll
5563 if (is_multiline)
5564 {
5565 // Test if cursor is vertically visible
5566 if (cursor_offset.y - g.FontSize < scroll_y)
5567 new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5568 else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5569 new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5570 }
5571 state->CursorFollow = false;
5572 }
5573 if (state->CursorCenterY)
5574 {
5575 if (is_multiline)
5576 new_scroll_y = cursor_offset.y - g.FontSize - (inner_size.y * 0.5f - style.FramePadding.y);
5577 state->CursorCenterY = false;
5578 render_cursor = false;
5579 }
5580 if (new_scroll_y != scroll_y)
5581 {
5582 const float scroll_max_y = ImMax((text_size_y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5583 scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
5584 draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5585 draw_window->Scroll.y = scroll_y;
5586 CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5587 line_visible_n1 = ImMin(line_visible_n1, line_count);
5588 }
5589
5590 // Draw selection
5591 draw_scroll.x = state->Scroll.x;
5592 if (render_selection)
5593 {
5594 const ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
5595 const float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME-DPI: those offsets should be part of the style? they don't play so well with multi-line selection.
5596 const float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5597 const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5598
5599 const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end);
5600 const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end);
5601 for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++)
5602 {
5603 const char* p = line_index->get_line_begin(buf_display, line_n);
5604 const char* p_eol = line_index->get_line_end(buf_display, line_n);
5605 const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n');
5606 if (p_eol_is_wrap)
5607 p_eol++;
5608 const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
5609 const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
5610
5611 float rect_width = 0.0f;
5612 if (line_selected_begin < line_selected_end)
5613 rect_width += CalcTextSize(line_selected_begin, line_selected_end).x;
5614 if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap)
5615 rect_width += bg_eol_width; // So we can see selected empty lines
5616 if (rect_width == 0.0f)
5617 continue;
5618
5619 ImRect rect;
5620 rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x;
5621 rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize;
5622 rect.Max.x = rect.Min.x + rect_width;
5623 rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize;
5624 rect.Min.y += bg_offy_up;
5625 rect.ClipWith(clip_rect);
5626 draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5627 }
5628 }
5629 }
5630
5631 // Find render position for right alignment (single-line only)
5632 if (g.ActiveId != id && (flags & ImGuiInputTextFlags_ElideLeft) && !render_cursor && !render_selection)
5633 draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5634 //draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive?
5635
5636 // Render text
5637 if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
5638 g.Font->RenderText(draw_window->DrawList, g.FontSize,
5639 draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
5640 text_col, clip_rect.AsVec4(),
5641 line_index->get_line_begin(buf_display, line_visible_n0),
5642 line_index->get_line_end(buf_display, line_visible_n1 - 1),
5643 wrap_width, ImDrawTextFlags_WrapKeepBlanks | ImDrawTextFlags_CpuFineClip);
5644
5645 // Render blinking cursor
5646 if (render_cursor)
5647 {
5648 state->CursorAnim += io.DeltaTime;
5649 bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5650 ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5651 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);
5652 if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5653 draw_window->DrawList->AddLineV(cursor_screen_rect.Min.x, cursor_screen_rect.Min.y, cursor_screen_rect.Max.y, GetColorU32(ImGuiCol_InputTextCursor), style.InputTextCursorSize);
5654
5655 // 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.)
5656 // This is required for some backends (SDL3) to start emitting character/text inputs.
5657 // As per #6341, make sure we don't set that on the deactivating frame.
5658 if (!is_readonly && g.ActiveId == id)
5659 {
5660 ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
5661 ime_data->WantVisible = true;
5662 ime_data->WantTextInput = true;
5663 ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5664 ime_data->InputLineHeight = g.FontSize;
5665 ime_data->ViewportId = window->Viewport->ID;
5666 }
5667 }
5668
5669 if (is_password && !is_displaying_hint)
5670 PopPasswordFont();
5671
5672 if (is_multiline)
5673 {
5674 // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5675 Dummy(ImVec2(0.0f, text_size_y + style.FramePadding.y));
5676 g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5677 EndChild();
5678 item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5679
5680 // ...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...
5681 // FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5682 EndGroup();
5683 if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5684 {
5685 g.LastItemData.ID = id;
5686 g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5687 g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5688 }
5689 }
5690 if (state && is_readonly)
5691 state->TextSrc = NULL;
5692
5693 // Log as text
5694 if (g.LogEnabled && (!is_password || is_displaying_hint))
5695 {
5696 LogSetNextTextDecoration("{", "}");
5697 LogRenderedText(&draw_pos, buf_display, buf_display_end);
5698 }
5699
5700 if (label_size.x > 0)
5701 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label, label_end, false);
5702
5703 if (value_changed)
5704 MarkItemEdited(id);
5705
5706 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5707 if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5708 return validated;
5709 else
5710 return value_changed;
5711}
5712
5713void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5714{
5715#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5716 ImGuiContext& g = *GImGui;
5717 ImStb::STB_TexteditState* stb_state = state->Stb;
5718 ImStb::StbUndoState* undo_state = &stb_state->undostate;
5719 Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5720 DebugLocateItemOnHover(state->ID);
5721 Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state->TextLen, stb_state->cursor,
5722 (state->Flags & ImGuiInputTextFlags_WordWrap) ? (state->LastMoveDirectionLR == ImGuiDir_Left ? " (L)" : " (R)") : "",
5723 stb_state->select_start, stb_state->select_end);
5724 Text("BufCapacity: %d, LineCount: %d", state->BufCapacity, state->LineCount);
5725 Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5726 Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5727 Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point);
5728 if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5729 {
5730 PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5731 for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5732 {
5733 ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5734 const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5735 if (undo_rec_type == ' ')
5736 BeginDisabled();
5737 const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5738 const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5739 Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5740 undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf_preview_len, buf_preview_str);
5741 if (undo_rec_type == ' ')
5742 EndDisabled();
5743 }
5744 PopStyleVar();
5745 }
5746 EndChild();
5747#else
5748 IM_UNUSED(state);
5749#endif
5750}
5751
5752//-------------------------------------------------------------------------
5753// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5754//-------------------------------------------------------------------------
5755// - ColorEdit3()
5756// - ColorEdit4()
5757// - ColorPicker3()
5758// - RenderColorRectWithAlphaCheckerboard() [Internal]
5759// - ColorPicker4()
5760// - ColorButton()
5761// - SetColorEditOptions()
5762// - ColorTooltip() [Internal]
5763// - ColorEditOptionsPopup() [Internal]
5764// - ColorPickerOptionsPopup() [Internal]
5765//-------------------------------------------------------------------------
5766
5767bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5768{
5769 return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5770}
5771
5772static void ColorEditRestoreH(const float* col, float* H)
5773{
5774 ImGuiContext& g = *GImGui;
5775 IM_ASSERT(g.ColorEditCurrentID != 0);
5776 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5777 return;
5778 *H = g.ColorEditSavedHue;
5779}
5780
5781// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5782// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5783static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5784{
5785 ImGuiContext& g = *GImGui;
5786 IM_ASSERT(g.ColorEditCurrentID != 0);
5787 if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5788 return;
5789
5790 // When S == 0, H is undefined.
5791 // When H == 1 it wraps around to 0.
5792 if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5793 *H = g.ColorEditSavedHue;
5794
5795 // When V == 0, S is undefined.
5796 if (*V == 0.0f)
5797 *S = g.ColorEditSavedSat;
5798}
5799
5800// Edit colors components (each component in 0.0f..1.0f range).
5801// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5802// 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.
5803bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5804{
5805 ImGuiWindow* window = GetCurrentWindow();
5806 if (window->SkipItems)
5807 return false;
5808
5809 ImGuiContext& g = *GImGui;
5810 const ImGuiStyle& style = g.Style;
5811 const float square_sz = GetFrameHeight();
5812 const char* label_display_end = FindRenderedTextEnd(label);
5813 float w_full = CalcItemWidth();
5814 g.NextItemData.ClearFlags();
5815
5816 BeginGroup();
5817 PushID(label);
5818 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5819 if (set_current_color_edit_id)
5820 g.ColorEditCurrentID = window->IDStack.back();
5821
5822 // If we're not showing any slider there's no point in doing any HSV conversions
5823 const ImGuiColorEditFlags flags_untouched = flags;
5824 if (flags & ImGuiColorEditFlags_NoInputs)
5825 flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5826
5827 // Context menu: display and modify options (before defaults are applied)
5828 if (!(flags & ImGuiColorEditFlags_NoOptions))
5829 ColorEditOptionsPopup(col, flags);
5830
5831 // Read stored options
5832 if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5833 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5834 if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5835 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5836 if (!(flags & ImGuiColorEditFlags_PickerMask_))
5837 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5838 if (!(flags & ImGuiColorEditFlags_InputMask_))
5839 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5840 flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5841 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5842 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5843
5844 const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5845 const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5846 const int components = alpha ? 4 : 3;
5847 const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5848 const float w_inputs = ImMax(w_full - w_button, 1.0f);
5849 w_full = w_inputs + w_button;
5850
5851 // Convert to the formats we need
5852 float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5853 if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5854 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5855 else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5856 {
5857 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5858 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5859 ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5860 }
5861 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]) };
5862
5863 bool value_changed = false;
5864 bool value_changed_as_float = false;
5865
5866 const ImVec2 pos = window->DC.CursorPos;
5867 const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5868 window->DC.CursorPos.x = pos.x + inputs_offset_x;
5869
5870 if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5871 {
5872 // RGB/HSV 0..255 Sliders
5873 const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5874 const float w_per_component = IM_TRUNC(w_items / components);
5875 const bool draw_color_marker = (flags & (ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_NoColorMarkers)) == 0;
5876
5877 const bool hide_prefix = draw_color_marker || (w_per_component <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5878 static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5879 static const char* fmt_table_int[3][4] =
5880 {
5881 { "%3d", "%3d", "%3d", "%3d" }, // Short display
5882 { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5883 { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5884 };
5885 static const char* fmt_table_float[3][4] =
5886 {
5887 { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5888 { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5889 { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5890 };
5891 const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5892 const ImGuiSliderFlags drag_flags = draw_color_marker ? ImGuiSliderFlags_ColorMarkers : ImGuiSliderFlags_None;
5893
5894 float prev_split = 0.0f;
5895 for (int n = 0; n < components; n++)
5896 {
5897 if (n > 0)
5898 SameLine(0, style.ItemInnerSpacing.x);
5899 float next_split = IM_TRUNC(w_items * (n + 1) / components);
5900 SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5901 prev_split = next_split;
5902 if (draw_color_marker)
5903 SetNextItemColorMarker(GDefaultRgbaColorMarkers[n]);
5904
5905 // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5906 if (flags & ImGuiColorEditFlags_Float)
5907 {
5908 value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n], drag_flags);
5909 value_changed_as_float |= value_changed;
5910 }
5911 else
5912 {
5913 value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n], drag_flags);
5914 }
5915 if (!(flags & ImGuiColorEditFlags_NoOptions))
5916 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5917 }
5918 }
5919 else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5920 {
5921 // RGB Hexadecimal Input
5922 char buf[64];
5923 if (alpha)
5924 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255));
5925 else
5926 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5927 SetNextItemWidth(w_inputs);
5928 if (InputText("##Text", buf, IM_COUNTOF(buf), ImGuiInputTextFlags_CharsUppercase))
5929 {
5930 value_changed = true;
5931 char* p = buf;
5932 while (*p == '#' || ImCharIsBlankA(*p))
5933 p++;
5934 i[0] = i[1] = i[2] = 0;
5935 i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5936 int r;
5937 if (alpha)
5938 r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned)
5939 else
5940 r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5941 IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5942 }
5943 if (!(flags & ImGuiColorEditFlags_NoOptions))
5944 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5945 }
5946
5947 ImGuiWindow* picker_active_window = NULL;
5948 if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5949 {
5950 const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5951 window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5952
5953 const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5954 if (ColorButton("##ColorButton", col_v4, flags))
5955 {
5956 if (!(flags & ImGuiColorEditFlags_NoPicker))
5957 {
5958 // Store current color and open a picker
5959 g.ColorPickerRef = col_v4;
5960 OpenPopup("picker");
5961 SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5962 }
5963 }
5964 if (!(flags & ImGuiColorEditFlags_NoOptions))
5965 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5966
5967 if (BeginPopup("picker"))
5968 {
5969 if (g.CurrentWindow->BeginCount == 1)
5970 {
5971 picker_active_window = g.CurrentWindow;
5972 if (label != label_display_end)
5973 {
5974 TextEx(label, label_display_end);
5975 Spacing();
5976 }
5977 ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5978 ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5979 SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5980 value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
5981 }
5982 EndPopup();
5983 }
5984 }
5985
5986 if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5987 {
5988 // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5989 // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5990 SameLine(0.0f, style.ItemInnerSpacing.x);
5991 window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5992 TextEx(label, label_display_end);
5993 }
5994
5995 // Convert back
5996 if (value_changed && picker_active_window == NULL)
5997 {
5998 if (!value_changed_as_float)
5999 for (int n = 0; n < 4; n++)
6000 f[n] = i[n] / 255.0f;
6001 if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
6002 {
6003 g.ColorEditSavedHue = f[0];
6004 g.ColorEditSavedSat = f[1];
6005 ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
6006 g.ColorEditSavedID = g.ColorEditCurrentID;
6007 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
6008 }
6009 if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
6010 ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
6011
6012 col[0] = f[0];
6013 col[1] = f[1];
6014 col[2] = f[2];
6015 if (alpha)
6016 col[3] = f[3];
6017 }
6018
6019 if (set_current_color_edit_id)
6020 g.ColorEditCurrentID = 0;
6021 PopID();
6022 EndGroup();
6023
6024 // Drag and Drop Target
6025 // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
6026 if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
6027 {
6028 bool accepted_drag_drop = false;
6029 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
6030 {
6031 memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
6032 value_changed = accepted_drag_drop = true;
6033 }
6034 if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
6035 {
6036 memcpy((float*)col, payload->Data, sizeof(float) * components);
6037 value_changed = accepted_drag_drop = true;
6038 }
6039
6040 // Drag-drop payloads are always RGB
6041 if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
6042 ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
6043 EndDragDropTarget();
6044 }
6045
6046 // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
6047 if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
6048 g.LastItemData.ID = g.ActiveId;
6049
6050 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6051 MarkItemEdited(g.LastItemData.ID);
6052
6053 return value_changed;
6054}
6055
6056bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
6057{
6058 float col4[4] = { col[0], col[1], col[2], 1.0f };
6059 if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
6060 return false;
6061 col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
6062 return true;
6063}
6064
6065// Helper for ColorPicker4()
6066static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
6067{
6068 ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
6069 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32(0,0,0,alpha8));
6070 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
6071 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32(0,0,0,alpha8));
6072 ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
6073}
6074
6075// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6076// (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.)
6077// 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..)
6078// 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)
6079bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
6080{
6081 ImGuiContext& g = *GImGui;
6082 ImGuiWindow* window = GetCurrentWindow();
6083 if (window->SkipItems)
6084 return false;
6085
6086 ImDrawList* draw_list = window->DrawList;
6087 ImGuiStyle& style = g.Style;
6088 ImGuiIO& io = g.IO;
6089
6090 const float width = CalcItemWidth();
6091 const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
6092 g.NextItemData.ClearFlags();
6093
6094 PushID(label);
6095 const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
6096 if (set_current_color_edit_id)
6097 g.ColorEditCurrentID = window->IDStack.back();
6098 BeginGroup();
6099
6100 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6101 flags |= ImGuiColorEditFlags_NoSmallPreview;
6102
6103 // Context menu: display and store options.
6104 if (!(flags & ImGuiColorEditFlags_NoOptions))
6105 ColorPickerOptionsPopup(col, flags);
6106
6107 // Read stored options
6108 if (!(flags & ImGuiColorEditFlags_PickerMask_))
6109 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
6110 if (!(flags & ImGuiColorEditFlags_InputMask_))
6111 flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
6112 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
6113 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
6114 if (!(flags & ImGuiColorEditFlags_NoOptions))
6115 flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
6116
6117 // Setup
6118 int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
6119 bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
6120 ImVec2 picker_pos = window->DC.CursorPos;
6121 float square_sz = GetFrameHeight();
6122 float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
6123 float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
6124 float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
6125 float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
6126 float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
6127
6128 float backup_initial_col[4];
6129 memcpy(backup_initial_col, col, components * sizeof(float));
6130
6131 float wheel_thickness = sv_picker_size * 0.08f;
6132 float wheel_r_outer = sv_picker_size * 0.50f;
6133 float wheel_r_inner = wheel_r_outer - wheel_thickness;
6134 ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
6135
6136 // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
6137 float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
6138 ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
6139 ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
6140 ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
6141
6142 float H = col[0], S = col[1], V = col[2];
6143 float R = col[0], G = col[1], B = col[2];
6144 if (flags & ImGuiColorEditFlags_InputRGB)
6145 {
6146 // Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
6147 ColorConvertRGBtoHSV(R, G, B, H, S, V);
6148 ColorEditRestoreHS(col, &H, &S, &V);
6149 }
6150 else if (flags & ImGuiColorEditFlags_InputHSV)
6151 {
6152 ColorConvertHSVtoRGB(H, S, V, R, G, B);
6153 }
6154
6155 bool value_changed = false, value_changed_h = false, value_changed_sv = false;
6156
6157 PushItemFlag(ImGuiItemFlags_NoNav, true);
6158 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6159 {
6160 // Hue wheel + SV triangle logic
6161 InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
6162 if (IsItemActive() && !is_readonly)
6163 {
6164 ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
6165 ImVec2 current_off = g.IO.MousePos - wheel_center;
6166 float initial_dist2 = ImLengthSqr(initial_off);
6167 if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
6168 {
6169 // Interactive with Hue wheel
6170 H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
6171 if (H < 0.0f)
6172 H += 1.0f;
6173 value_changed = value_changed_h = true;
6174 }
6175 float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
6176 float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
6177 if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
6178 {
6179 // Interacting with SV triangle
6180 ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
6181 if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
6182 current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
6183 float uu, vv, ww;
6184 ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
6185 V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
6186 S = ImClamp(uu / V, 0.0001f, 1.0f);
6187 value_changed = value_changed_sv = true;
6188 }
6189 }
6190 if (!(flags & ImGuiColorEditFlags_NoOptions))
6191 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6192 }
6193 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6194 {
6195 // SV rectangle logic
6196 InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
6197 if (IsItemActive() && !is_readonly)
6198 {
6199 S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
6200 V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6201 ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
6202 value_changed = value_changed_sv = true;
6203 }
6204 if (!(flags & ImGuiColorEditFlags_NoOptions))
6205 OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6206
6207 // Hue bar logic
6208 SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
6209 InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
6210 if (IsItemActive() && !is_readonly)
6211 {
6212 H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6213 value_changed = value_changed_h = true;
6214 }
6215 }
6216
6217 // Alpha bar logic
6218 if (alpha_bar)
6219 {
6220 SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
6221 InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
6222 if (IsItemActive())
6223 {
6224 col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6225 value_changed = true;
6226 }
6227 }
6228 PopItemFlag(); // ImGuiItemFlags_NoNav
6229
6230 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6231 {
6232 SameLine(0, style.ItemInnerSpacing.x);
6233 BeginGroup();
6234 }
6235
6236 if (!(flags & ImGuiColorEditFlags_NoLabel))
6237 {
6238 const char* label_display_end = FindRenderedTextEnd(label);
6239 if (label != label_display_end)
6240 {
6241 if ((flags & ImGuiColorEditFlags_NoSidePreview))
6242 SameLine(0, style.ItemInnerSpacing.x);
6243 TextEx(label, label_display_end);
6244 }
6245 }
6246
6247 if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6248 {
6249 PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
6250 ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6251 if ((flags & ImGuiColorEditFlags_NoLabel))
6252 Text("Current");
6253
6254 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
6255 ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
6256 if (ref_col != NULL)
6257 {
6258 Text("Original");
6259 ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
6260 if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
6261 {
6262 memcpy(col, ref_col, components * sizeof(float));
6263 value_changed = true;
6264 }
6265 }
6266 PopItemFlag();
6267 EndGroup();
6268 }
6269
6270 // Convert back color to RGB
6271 if (value_changed_h || value_changed_sv)
6272 {
6273 if (flags & ImGuiColorEditFlags_InputRGB)
6274 {
6275 ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
6276 g.ColorEditSavedHue = H;
6277 g.ColorEditSavedSat = S;
6278 g.ColorEditSavedID = g.ColorEditCurrentID;
6279 g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
6280 }
6281 else if (flags & ImGuiColorEditFlags_InputHSV)
6282 {
6283 col[0] = H;
6284 col[1] = S;
6285 col[2] = V;
6286 }
6287 }
6288
6289 // R,G,B and H,S,V slider color editor
6290 bool value_changed_fix_hue_wrap = false;
6291 if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
6292 {
6293 PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6294 ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6295 ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6296 if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6297 if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
6298 {
6299 // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6300 // 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)
6301 value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6302 value_changed = true;
6303 }
6304 if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6305 value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
6306 if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6307 value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
6308 PopItemWidth();
6309 }
6310
6311 // Try to cancel hue wrap (after ColorEdit4 call), if any
6312 if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6313 {
6314 float new_H, new_S, new_V;
6315 ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
6316 if (new_H <= 0 && H > 0)
6317 {
6318 if (new_V <= 0 && V != new_V)
6319 ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
6320 else if (new_S <= 0)
6321 ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
6322 }
6323 }
6324
6325 if (value_changed)
6326 {
6327 if (flags & ImGuiColorEditFlags_InputRGB)
6328 {
6329 R = col[0];
6330 G = col[1];
6331 B = col[2];
6332 ColorConvertRGBtoHSV(R, G, B, H, S, V);
6333 ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
6334 }
6335 else if (flags & ImGuiColorEditFlags_InputHSV)
6336 {
6337 H = col[0];
6338 S = col[1];
6339 V = col[2];
6340 ColorConvertHSVtoRGB(H, S, V, R, G, B);
6341 }
6342 }
6343
6344 const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6345 const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6346 const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6347 const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6348 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) };
6349
6350 ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z);
6351 ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
6352 ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6353
6354 ImVec2 sv_cursor_pos;
6355
6356 if (flags & ImGuiColorEditFlags_PickerHueWheel)
6357 {
6358 // Render Hue Wheel
6359 const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6360 const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
6361 for (int n = 0; n < 6; n++)
6362 {
6363 const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6364 const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6365 const int vert_start_idx = draw_list->VtxBuffer.Size;
6366 draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6367 draw_list->PathStroke(col_white, wheel_thickness);
6368 const int vert_end_idx = draw_list->VtxBuffer.Size;
6369
6370 // Paint colors over existing vertices
6371 ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6372 ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6373 ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6374 }
6375
6376 // Render Cursor + preview on Hue Wheel
6377 float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6378 float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6379 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);
6380 float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6381 int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6382 draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6383 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6384 draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6385
6386 // Render SV triangle (rotated according to hue)
6387 ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6388 ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6389 ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6390 ImVec2 uv_white = GetFontTexUvWhitePixel();
6391 draw_list->PrimReserve(3, 3);
6392 draw_list->PrimVtx(tra, uv_white, hue_color32);
6393 draw_list->PrimVtx(trb, uv_white, col_black);
6394 draw_list->PrimVtx(trc, uv_white, col_white);
6395 draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6396 sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6397 }
6398 else if (flags & ImGuiColorEditFlags_PickerHueBar)
6399 {
6400 // Render SV Square
6401 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6402 draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6403 RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6404 sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much
6405 sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2);
6406
6407 // Render Hue Bar
6408 for (int i = 0; i < 6; ++i)
6409 draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]);
6410 float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6411 RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6412 RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha);
6413 }
6414
6415 // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6416 float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6417 int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6418 draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6419 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6420 draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6421
6422 // Render alpha bar
6423 if (alpha_bar)
6424 {
6425 float alpha = ImSaturate(col[3]);
6426 ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6427 RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6428 draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK);
6429 float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6430 RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6431 RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha);
6432 }
6433
6434 EndGroup();
6435
6436 if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6437 value_changed = false;
6438 if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6439 MarkItemEdited(g.LastItemData.ID);
6440
6441 if (set_current_color_edit_id)
6442 g.ColorEditCurrentID = 0;
6443 PopID();
6444
6445 return value_changed;
6446}
6447
6448// A little color square. Return true when clicked.
6449// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6450// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6451// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6452bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6453{
6454 ImGuiWindow* window = GetCurrentWindow();
6455 if (window->SkipItems)
6456 return false;
6457
6458 ImGuiContext& g = *GImGui;
6459 const ImGuiID id = window->GetID(desc_id);
6460 const float default_size = GetFrameHeight();
6461 const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6462 const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6463 ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6464 if (!ItemAdd(bb, id))
6465 return false;
6466
6467 bool hovered, held;
6468 bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6469
6470 if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6471 flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6472
6473 ImVec4 col_rgb = col;
6474 if (flags & ImGuiColorEditFlags_InputHSV)
6475 ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6476
6477 ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6478 float grid_step = ImMin(size.x, size.y) / 2.99f;
6479 float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6480 ImRect bb_inner = bb;
6481 float off = 0.0f;
6482 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6483 {
6484 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.
6485 bb_inner.Expand(off);
6486 }
6487 if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6488 {
6489 float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6490 if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6491 RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight);
6492 else
6493 window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight);
6494 window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6495 }
6496 else
6497 {
6498 // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6499 ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6500 if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6501 RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6502 else
6503 window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6504 }
6505 RenderNavCursor(bb, id);
6506 if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6507 {
6508 if (g.Style.FrameBorderSize > 0.0f)
6509 RenderFrameBorder(bb.Min, bb.Max, rounding);
6510 else
6511 window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding, 1.0f * (float)(int)g.Style._MainScale); // Color buttons are often in need of some sort of border // FIXME-DPI
6512 }
6513
6514 // Drag and Drop Source
6515 // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6516 if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6517 {
6518 if (flags & ImGuiColorEditFlags_NoAlpha)
6519 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6520 else
6521 SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6522 ColorButton(desc_id, col, flags);
6523 SameLine();
6524 TextEx("Color");
6525 EndDragDropSource();
6526 }
6527
6528 // Tooltip
6529 if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6530 ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6531
6532 return pressed;
6533}
6534
6535// Initialize/override default color options
6536// FIXME: Could be moved to a simple IO field.
6537void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6538{
6539 ImGuiContext& g = *GImGui;
6540 if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6541 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6542 if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6543 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6544 if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6545 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6546 if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6547 flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6548 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6549 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6550 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6551 IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6552 g.ColorEditOptions = flags;
6553}
6554
6555// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6556void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6557{
6558 ImGuiContext& g = *GImGui;
6559
6560 if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6561 return;
6562 const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6563 if (text_end > text)
6564 {
6565 TextEx(text, text_end);
6566 Separator();
6567 }
6568
6569 ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6570 ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6571 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]);
6572 ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6573 ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz);
6574 SameLine();
6575 if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6576 {
6577 if (flags & ImGuiColorEditFlags_NoAlpha)
6578 Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]);
6579 else
6580 Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]);
6581 }
6582 else if (flags & ImGuiColorEditFlags_InputHSV)
6583 {
6584 if (flags & ImGuiColorEditFlags_NoAlpha)
6585 Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6586 else
6587 Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6588 }
6589 EndTooltip();
6590}
6591
6592void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6593{
6594 bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6595 bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6596 if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6597 return;
6598
6599 ImGuiContext& g = *GImGui;
6600 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6601 ImGuiColorEditFlags opts = g.ColorEditOptions;
6602 if (allow_opt_inputs)
6603 {
6604 if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6605 if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6606 if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6607 }
6608 if (allow_opt_datatype)
6609 {
6610 if (allow_opt_inputs) Separator();
6611 if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6612 if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6613 }
6614
6615 if (allow_opt_inputs || allow_opt_datatype)
6616 Separator();
6617 if (Button("Copy as..", ImVec2(-1, 0)))
6618 OpenPopup("Copy");
6619 if (BeginPopup("Copy"))
6620 {
6621 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]);
6622 char buf[64];
6623 ImFormatString(buf, IM_COUNTOF(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6624 if (Selectable(buf))
6625 SetClipboardText(buf);
6626 ImFormatString(buf, IM_COUNTOF(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6627 if (Selectable(buf))
6628 SetClipboardText(buf);
6629 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", cr, cg, cb);
6630 if (Selectable(buf))
6631 SetClipboardText(buf);
6632 if (!(flags & ImGuiColorEditFlags_NoAlpha))
6633 {
6634 ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6635 if (Selectable(buf))
6636 SetClipboardText(buf);
6637 }
6638 EndPopup();
6639 }
6640
6641 g.ColorEditOptions = opts;
6642 PopItemFlag();
6643 EndPopup();
6644}
6645
6646void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6647{
6648 bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6649 bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6650 if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6651 return;
6652
6653 ImGuiContext& g = *GImGui;
6654 PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6655 if (allow_opt_picker)
6656 {
6657 ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function
6658 PushItemWidth(picker_size.x);
6659 for (int picker_type = 0; picker_type < 2; picker_type++)
6660 {
6661 // Draw small/thumbnail version of each picker type (over an invisible button for selection)
6662 if (picker_type > 0) Separator();
6663 PushID(picker_type);
6664 ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6665 if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6666 if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6667 ImVec2 backup_pos = GetCursorScreenPos();
6668 if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6669 g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6670 SetCursorScreenPos(backup_pos);
6671 ImVec4 previewing_ref_col;
6672 memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6673 ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6674 PopID();
6675 }
6676 PopItemWidth();
6677 }
6678 if (allow_opt_alpha_bar)
6679 {
6680 if (allow_opt_picker) Separator();
6681 CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
6682 }
6683 PopItemFlag();
6684 EndPopup();
6685}
6686
6687//-------------------------------------------------------------------------
6688// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6689//-------------------------------------------------------------------------
6690// - TreeNode()
6691// - TreeNodeV()
6692// - TreeNodeEx()
6693// - TreeNodeExV()
6694// - TreeNodeStoreStackData() [Internal]
6695// - TreeNodeBehavior() [Internal]
6696// - TreePush()
6697// - TreePop()
6698// - GetTreeNodeToLabelSpacing()
6699// - SetNextItemOpen()
6700// - CollapsingHeader()
6701//-------------------------------------------------------------------------
6702
6703bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6704{
6705 va_list args;
6706 va_start(args, fmt);
6707 bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6708 va_end(args);
6709 return is_open;
6710}
6711
6712bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6713{
6714 va_list args;
6715 va_start(args, fmt);
6716 bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6717 va_end(args);
6718 return is_open;
6719}
6720
6721bool ImGui::TreeNode(const char* label)
6722{
6723 ImGuiWindow* window = GetCurrentWindow();
6724 if (window->SkipItems)
6725 return false;
6726 ImGuiID id = window->GetID(label);
6727 return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6728}
6729
6730bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6731{
6732 return TreeNodeExV(str_id, 0, fmt, args);
6733}
6734
6735bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6736{
6737 return TreeNodeExV(ptr_id, 0, fmt, args);
6738}
6739
6740bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6741{
6742 ImGuiWindow* window = GetCurrentWindow();
6743 if (window->SkipItems)
6744 return false;
6745 ImGuiID id = window->GetID(label);
6746 return TreeNodeBehavior(id, flags, label, NULL);
6747}
6748
6749bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6750{
6751 va_list args;
6752 va_start(args, fmt);
6753 bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6754 va_end(args);
6755 return is_open;
6756}
6757
6758bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6759{
6760 va_list args;
6761 va_start(args, fmt);
6762 bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6763 va_end(args);
6764 return is_open;
6765}
6766
6767bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6768{
6769 ImGuiWindow* window = GetCurrentWindow();
6770 if (window->SkipItems)
6771 return false;
6772
6773 ImGuiID id = window->GetID(str_id);
6774 const char* label, *label_end;
6775 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6776 return TreeNodeBehavior(id, flags, label, label_end);
6777}
6778
6779bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6780{
6781 ImGuiWindow* window = GetCurrentWindow();
6782 if (window->SkipItems)
6783 return false;
6784
6785 ImGuiID id = window->GetID(ptr_id);
6786 const char* label, *label_end;
6787 ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6788 return TreeNodeBehavior(id, flags, label, label_end);
6789}
6790
6791// The reason those two functions are not yet in public API is because I would like to design a more feature-full and generic API for this.
6792// They are otherwise function (cc: #3823, #9251, #7553, #6754, #5423, #2958, #2079, #1947, #1131, #722)
6793bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6794{
6795 ImGuiContext& g = *GImGui;
6796 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6797 return storage->GetInt(storage_id, 0) != 0;
6798}
6799
6800void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool is_open)
6801{
6802 ImGuiContext& g = *GImGui;
6803 ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6804 storage->SetInt(storage_id, is_open ? 1 : 0);
6805}
6806
6807bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6808{
6809 // Leaf node always open a new tree/id scope. If you never use it, add ImGuiTreeNodeFlags_NoTreePushOnOpen.
6810 if (flags & ImGuiTreeNodeFlags_Leaf)
6811 return true;
6812
6813 // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6814 ImGuiContext& g = *GImGui;
6815 ImGuiWindow* window = g.CurrentWindow;
6816 ImGuiStorage* storage = window->DC.StateStorage;
6817
6818 bool is_open;
6819 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6820 {
6821 if (g.NextItemData.OpenCond & ImGuiCond_Always)
6822 {
6823 is_open = g.NextItemData.OpenVal;
6824 TreeNodeSetOpen(storage_id, is_open);
6825 }
6826 else
6827 {
6828 // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6829 const int stored_value = storage->GetInt(storage_id, -1);
6830 if (stored_value == -1)
6831 {
6832 is_open = g.NextItemData.OpenVal;
6833 TreeNodeSetOpen(storage_id, is_open);
6834 }
6835 else
6836 {
6837 is_open = stored_value != 0;
6838 }
6839 }
6840 }
6841 else
6842 {
6843 is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6844 }
6845
6846 // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6847 // NB- If we are above max depth we still allow manually opened nodes to be logged.
6848 if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6849 is_open = true;
6850
6851 return is_open;
6852}
6853
6854// Store ImGuiTreeNodeStackData for just submitted node.
6855// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6856static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
6857{
6858 ImGuiContext& g = *GImGui;
6859 ImGuiWindow* window = g.CurrentWindow;
6860
6861 g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6862 ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6863 tree_node_data->ID = g.LastItemData.ID;
6864 tree_node_data->TreeFlags = flags;
6865 tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6866 tree_node_data->NavRect = g.LastItemData.NavRect;
6867
6868 // Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
6869 const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
6870 tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
6871 tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
6872 tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
6873 window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6874 if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
6875 window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
6876}
6877
6878bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6879{
6880 ImGuiWindow* window = GetCurrentWindow();
6881 if (window->SkipItems)
6882 return false;
6883
6884 ImGuiContext& g = *GImGui;
6885 const ImGuiStyle& style = g.Style;
6886
6887 // When not framed, we vertically increase height up to typical framed widget height
6888 const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6889 const bool use_frame_padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding));
6890 const ImVec2 padding = use_frame_padding ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6891
6892 if (!label_end)
6893 label_end = FindRenderedTextEnd(label);
6894 const ImVec2 label_size = CalcTextSize(label, label_end, false);
6895
6896 const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6897 const float text_offset_y = use_frame_padding ? ImMax(style.FramePadding.y, window->DC.CurrLineTextBaseOffset) : window->DC.CurrLineTextBaseOffset; // Latch before ItemSize changes it
6898 const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6899
6900 const float frame_height = label_size.y + padding.y * 2;
6901 const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6902 const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6903 ImRect frame_bb;
6904 frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6905 frame_bb.Min.y = window->DC.CursorPos.y + (text_offset_y - padding.y);
6906 frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanLabelWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x;
6907 frame_bb.Max.y = window->DC.CursorPos.y + (text_offset_y - padding.y) + frame_height;
6908 if (display_frame)
6909 {
6910 const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6911 frame_bb.Min.x -= outer_extend;
6912 frame_bb.Max.x += outer_extend;
6913 }
6914
6915 ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6916 ItemSize(ImVec2(text_width, frame_height), padding.y);
6917
6918 // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6919 ImRect interact_bb = frame_bb;
6920 if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6921 interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6922
6923 // Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6924 ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6925 bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6926
6927 bool is_visible;
6928 if (span_all_columns || span_all_columns_label)
6929 {
6930 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6931 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6932 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6933 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6934 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6935 is_visible = ItemAdd(interact_bb, id);
6936 window->ClipRect.Min.x = backup_clip_rect_min_x;
6937 window->ClipRect.Max.x = backup_clip_rect_max_x;
6938 }
6939 else
6940 {
6941 is_visible = ItemAdd(interact_bb, id);
6942 }
6943 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6944 g.LastItemData.DisplayRect = frame_bb;
6945
6946 // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
6947 // Store data for the current depth to allow returning to this node from any child item.
6948 // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6949 // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
6950 bool store_tree_node_stack_data = false;
6951 if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
6952 flags |= g.Style.TreeLinesFlags;
6953 const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
6954 if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6955 {
6956 store_tree_node_stack_data = draw_tree_lines;
6957 if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
6958 if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6959 store_tree_node_stack_data = true;
6960 }
6961
6962 const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6963 if (!is_visible)
6964 {
6965 if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
6966 {
6967 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6968 parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, window->DC.CursorPos.y); // Don't need to aim to mid Y position as we are clipped anyway.
6969 if (frame_bb.Min.y >= window->ClipRect.Max.y)
6970 window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
6971 }
6972 if (is_open && store_tree_node_stack_data)
6973 TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6974 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6975 TreePushOverrideID(id);
6976 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6977 return is_open;
6978 }
6979
6980 if (span_all_columns || span_all_columns_label)
6981 {
6982 TablePushBackgroundChannel();
6983 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6984 g.LastItemData.ClipRect = window->ClipRect;
6985 }
6986
6987 ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6988 if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6989 button_flags |= ImGuiButtonFlags_AllowOverlap;
6990 if (!is_leaf)
6991 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6992
6993 // We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6994 // allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6995 // When clicking on the rest of the tree node we always disallow keyboard modifiers.
6996 const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6997 const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6998 const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6999
7000 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7001 if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
7002 flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
7003
7004 // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
7005 // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
7006 // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
7007 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
7008 // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
7009 // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
7010 // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
7011 // It is rather standard that arrow click react on Down rather than Up.
7012 // 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.
7013 if (is_mouse_x_over_arrow)
7014 button_flags |= ImGuiButtonFlags_PressedOnClick;
7015 else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
7016 button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
7017 else
7018 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7019 if (flags & ImGuiTreeNodeFlags_NoNavFocus)
7020 button_flags |= ImGuiButtonFlags_NoNavFocus;
7021
7022 bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
7023 const bool was_selected = selected;
7024
7025 // Multi-selection support (header)
7026 if (is_multi_select)
7027 {
7028 // Handle multi-select + alter button flags for it
7029 MultiSelectItemHeader(id, &selected, &button_flags);
7030 if (is_mouse_x_over_arrow)
7031 button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7032 }
7033 else
7034 {
7035 if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
7036 button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
7037 }
7038
7039 bool hovered, held;
7040 bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
7041 bool toggled = false;
7042 if (!is_leaf)
7043 {
7044 if (pressed && g.DragDropHoldJustPressedId != id)
7045 {
7046 if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
7047 toggled = true; // Single click
7048 if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
7049 toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
7050 if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
7051 toggled = true; // Double click
7052 }
7053 else if (pressed && g.DragDropHoldJustPressedId == id)
7054 {
7055 IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
7056 if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
7057 toggled = true;
7058 else
7059 pressed = false; // Cancel press so it doesn't trigger selection.
7060 }
7061
7062 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
7063 {
7064 toggled = true;
7065 NavClearPreferredPosForAxis(ImGuiAxis_X);
7066 NavMoveRequestCancel();
7067 }
7068 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?
7069 {
7070 toggled = true;
7071 NavClearPreferredPosForAxis(ImGuiAxis_X);
7072 NavMoveRequestCancel();
7073 }
7074
7075 if (toggled)
7076 {
7077 is_open = !is_open;
7078 window->DC.StateStorage->SetInt(storage_id, is_open);
7079 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
7080 }
7081 }
7082
7083 // Multi-selection support (footer)
7084 if (is_multi_select)
7085 {
7086 bool pressed_copy = pressed && !toggled;
7087 MultiSelectItemFooter(id, &selected, &pressed_copy);
7088 if (pressed)
7089 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
7090 }
7091
7092 if (selected != was_selected)
7093 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7094
7095 // Render
7096 {
7097 const ImU32 text_col = GetColorU32(ImGuiCol_Text);
7098 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
7099 if (is_multi_select)
7100 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7101 if (display_frame)
7102 {
7103 // Framed type
7104 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7105 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
7106 RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
7107 if (span_all_columns && !span_all_columns_label)
7108 TablePopBackgroundChannel();
7109 if (flags & ImGuiTreeNodeFlags_Bullet)
7110 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
7111 else if (!is_leaf)
7112 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f);
7113 else // Leaf without bullet, left-adjusted text
7114 text_pos.x -= text_offset_x - padding.x;
7115 if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
7116 frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
7117 if (g.LogEnabled)
7118 LogSetNextTextDecoration("###", "###");
7119 }
7120 else
7121 {
7122 // Unframed typed for tree nodes
7123 if (hovered || selected)
7124 {
7125 const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7126 RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false, 0.0f);
7127 }
7128 RenderNavCursor(frame_bb, id, nav_render_cursor_flags, 0.0f);
7129 if (span_all_columns && !span_all_columns_label)
7130 TablePopBackgroundChannel();
7131 if (flags & ImGuiTreeNodeFlags_Bullet)
7132 RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
7133 else if (!is_leaf)
7134 RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f);
7135 if (g.LogEnabled)
7136 LogSetNextTextDecoration(">", NULL);
7137 }
7138
7139 if (draw_tree_lines)
7140 TreeNodeDrawLineToChildNode(ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
7141
7142 // Label
7143 if (display_frame)
7144 RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
7145 else
7146 RenderText(text_pos, label, label_end, false);
7147
7148 if (span_all_columns_label)
7149 TablePopBackgroundChannel();
7150 }
7151
7152 if (is_open && store_tree_node_stack_data)
7153 TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
7154 if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
7155 TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
7156
7157 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
7158 return is_open;
7159}
7160
7161// Draw horizontal line from our parent node
7162// This is only called for visible child nodes so we are not too fussy anymore about performances
7163void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
7164{
7165 ImGuiContext& g = *GImGui;
7166 ImGuiWindow* window = g.CurrentWindow;
7167 if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
7168 return;
7169
7170 ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7171 float x1 = ImTrunc(parent_data->DrawLinesX1);
7172 float x2 = ImTrunc(target_pos.x - g.Style.ItemInnerSpacing.x);
7173 float y = ImTrunc(target_pos.y);
7174 float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(x2 - x1, g.Style.TreeLinesRounding) : 0.0f;
7175 parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, y - rounding);
7176 if (x1 >= x2)
7177 return;
7178 if (rounding > 0.0f)
7179 {
7180 x1 += 0.5f + rounding;
7181 window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
7182 if (x1 < x2)
7183 window->DrawList->PathLineTo(ImVec2(x2, y));
7184 window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7185 }
7186 else
7187 {
7188 window->DrawList->AddLineH(x1, x2, y, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7189 }
7190}
7191
7192// Draw vertical line of the hierarchy
7193void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
7194{
7195 ImGuiContext& g = *GImGui;
7196 ImGuiWindow* window = g.CurrentWindow;
7197 float y1 = ImMax(data->NavRect.Max.y, window->ClipRect.Min.y);
7198 float y2 = data->DrawLinesToNodesY2;
7199 if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
7200 {
7201 float y2_full = window->DC.CursorPos.y;
7202 if (g.CurrentTable)
7203 y2_full = ImMax(g.CurrentTable->RowPosY2, y2_full);
7204 y2_full = ImTrunc(y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
7205 if (y2 + (g.Style.ItemSpacing.y + g.Style.TreeLinesRounding) < y2_full) // FIXME: threshold to use ToNodes Y2 instead of Full Y2 when close by ItemSpacing.y
7206 y2 = y2_full;
7207 }
7208 y2 = ImMin(y2, window->ClipRect.Max.y);
7209 if (y2 <= y1)
7210 return;
7211 float x = ImTrunc(data->DrawLinesX1);
7212 if (data->DrawLinesTableColumn != -1)
7213 TablePushColumnChannel(data->DrawLinesTableColumn);
7214 window->DrawList->AddLineV(x, y1, y2, GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7215 if (data->DrawLinesTableColumn != -1)
7216 TablePopColumnChannel();
7217}
7218
7219void ImGui::TreePush(const char* str_id)
7220{
7221 ImGuiWindow* window = GetCurrentWindow();
7222 Indent();
7223 window->DC.TreeDepth++;
7224 PushID(str_id);
7225}
7226
7227void ImGui::TreePush(const void* ptr_id)
7228{
7229 ImGuiWindow* window = GetCurrentWindow();
7230 Indent();
7231 window->DC.TreeDepth++;
7232 PushID(ptr_id);
7233}
7234
7235void ImGui::TreePushOverrideID(ImGuiID id)
7236{
7237 ImGuiContext& g = *GImGui;
7238 ImGuiWindow* window = g.CurrentWindow;
7239 Indent();
7240 window->DC.TreeDepth++;
7241 PushOverrideID(id);
7242}
7243
7244void ImGui::TreePop()
7245{
7246 ImGuiContext& g = *GImGui;
7247 ImGuiWindow* window = g.CurrentWindow;
7248 Unindent();
7249
7250 window->DC.TreeDepth--;
7251 ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
7252
7253 if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
7254 {
7255 const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7256 IM_ASSERT(data->ID == window->IDStack.back());
7257
7258 // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
7259 if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
7260 if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
7261 NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
7262
7263 // Draw hierarchy lines
7264 if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
7265 TreeNodeDrawLineToTreePop(data);
7266
7267 g.TreeNodeStack.pop_back();
7268 window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
7269 window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
7270 }
7271
7272 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.
7273 PopID();
7274}
7275
7276// Horizontal distance preceding label when using TreeNode() or Bullet()
7277float ImGui::GetTreeNodeToLabelSpacing()
7278{
7279 ImGuiContext& g = *GImGui;
7280 return g.FontSize + (g.Style.FramePadding.x * 2.0f);
7281}
7282
7283// Set next TreeNode/CollapsingHeader open state.
7284void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
7285{
7286 ImGuiContext& g = *GImGui;
7287 if (g.CurrentWindow->SkipItems)
7288 return;
7289 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
7290 g.NextItemData.OpenVal = is_open;
7291 g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
7292}
7293
7294// Set next TreeNode/CollapsingHeader storage id.
7295void ImGui::SetNextItemStorageID(ImGuiID storage_id)
7296{
7297 ImGuiContext& g = *GImGui;
7298 if (g.CurrentWindow->SkipItems)
7299 return;
7300 g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
7301 g.NextItemData.StorageId = storage_id;
7302}
7303
7304// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
7305// 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().
7306bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
7307{
7308 ImGuiWindow* window = GetCurrentWindow();
7309 if (window->SkipItems)
7310 return false;
7311 ImGuiID id = window->GetID(label);
7312 return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
7313}
7314
7315// p_visible == NULL : regular collapsing header
7316// 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
7317// p_visible != NULL && *p_visible == false : do not show the header at all
7318// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
7319bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
7320{
7321 ImGuiWindow* window = GetCurrentWindow();
7322 if (window->SkipItems)
7323 return false;
7324
7325 if (p_visible && !*p_visible)
7326 return false;
7327
7328 ImGuiID id = window->GetID(label);
7329 flags |= ImGuiTreeNodeFlags_CollapsingHeader;
7330 if (p_visible)
7331 flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
7332 bool is_open = TreeNodeBehavior(id, flags, label);
7333 if (p_visible != NULL)
7334 {
7335 // Create a small overlapping close button
7336 // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
7337 // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
7338 ImGuiContext& g = *GImGui;
7339 ImGuiLastItemData last_item_backup = g.LastItemData;
7340 float button_size = g.FontSize;
7341 float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
7342 float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
7343 ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
7344 if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
7345 *p_visible = false;
7346 g.LastItemData = last_item_backup;
7347 }
7348
7349 return is_open;
7350}
7351
7352//-------------------------------------------------------------------------
7353// [SECTION] Widgets: Selectable
7354//-------------------------------------------------------------------------
7355// - Selectable()
7356//-------------------------------------------------------------------------
7357
7358// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
7359// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
7360// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
7361// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
7362bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7363{
7364 ImGuiWindow* window = GetCurrentWindow();
7365 if (window->SkipItems)
7366 return false;
7367
7368 ImGuiContext& g = *GImGui;
7369 const ImGuiStyle& style = g.Style;
7370
7371 // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
7372 ImGuiID id = window->GetID(label);
7373 const char* label_end = FindRenderedTextEnd(label);
7374 ImVec2 label_size = CalcTextSize(label, label_end, false);
7375 ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
7376 ImVec2 pos = window->DC.CursorPos;
7377 pos.y += window->DC.CurrLineTextBaseOffset;
7378 ItemSize(size, 0.0f);
7379
7380 // Fill horizontal space
7381 // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
7382 const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
7383 const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
7384 const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
7385 if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
7386 size.x = ImMax(label_size.x, max_x - min_x);
7387
7388 // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7389 // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7390 ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7391 if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7392 {
7393 const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7394 const float spacing_y = style.ItemSpacing.y;
7395 const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7396 const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7397 bb.Min.x -= spacing_L;
7398 bb.Min.y -= spacing_U;
7399 bb.Max.x += (spacing_x - spacing_L);
7400 bb.Max.y += (spacing_y - spacing_U);
7401 }
7402 //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7403
7404 const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7405 const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7406 bool is_visible;
7407 if (span_all_columns)
7408 {
7409 // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7410 const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7411 const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7412 window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7413 window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7414 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7415 window->ClipRect.Min.x = backup_clip_rect_min_x;
7416 window->ClipRect.Max.x = backup_clip_rect_max_x;
7417 }
7418 else
7419 {
7420 is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7421 }
7422
7423 const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7424 if (!is_visible)
7425 if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd)
7426 return false;
7427
7428 const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7429 if (disabled_item && !disabled_global) // Only testing this as an optimization
7430 BeginDisabled();
7431
7432 // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7433 // which would be advantageous since most selectable are not selected.
7434 if (span_all_columns)
7435 {
7436 if (g.CurrentTable)
7437 TablePushBackgroundChannel();
7438 else if (window->DC.CurrentColumns)
7439 PushColumnsBackground();
7440 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7441 g.LastItemData.ClipRect = window->ClipRect;
7442 }
7443
7444 // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7445 ImGuiButtonFlags button_flags = 0;
7446 if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7447 if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7448 if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7449 if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7450 if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7451 if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7452
7453 // Multi-selection support (header)
7454 const bool was_selected = selected;
7455 if (is_multi_select)
7456 {
7457 // Handle multi-select + alter button flags for it
7458 MultiSelectItemHeader(id, &selected, &button_flags);
7459 }
7460
7461 bool hovered, held;
7462 bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
7463 bool auto_selected = false;
7464
7465 // Multi-selection support (footer)
7466 if (is_multi_select)
7467 {
7468 MultiSelectItemFooter(id, &selected, &pressed);
7469 }
7470 else
7471 {
7472 // Auto-select when moved into
7473 // - This will be more fully fleshed in the range-select branch
7474 // - This is not exposed as it won't nicely work with some user side handling of shift/control
7475 // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7476 // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7477 // - (2) usage will fail with clipped items
7478 // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7479 if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7480 if (g.NavJustMovedToId == id && (g.NavJustMovedToKeyMods & ImGuiMod_Ctrl) == 0)
7481 selected = pressed = auto_selected = true;
7482 }
7483
7484 // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7485 if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7486 {
7487 if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7488 {
7489 SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7490 if (g.IO.ConfigNavCursorVisibleAuto)
7491 g.NavCursorVisible = false;
7492 }
7493 }
7494 if (pressed)
7495 MarkItemEdited(id);
7496
7497 if (selected != was_selected)
7498 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7499
7500 // Render
7501 if (is_visible)
7502 {
7503 const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7504 if (highlighted || selected)
7505 {
7506 // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7507 ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7508 RenderFrame(bb.Min, bb.Max, col, false, style.SelectableRounding);
7509 }
7510 if (g.NavId == id)
7511 {
7512 ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
7513 if (is_multi_select)
7514 nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7515 RenderNavCursor(bb, id, nav_render_cursor_flags, style.SelectableRounding);
7516 }
7517 }
7518
7519 if (span_all_columns)
7520 {
7521 if (g.CurrentTable)
7522 TablePopBackgroundChannel();
7523 else if (window->DC.CurrentColumns)
7524 PopColumnsBackground();
7525 }
7526
7527 // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7528 if (is_visible)
7529 RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, label_end, &label_size, style.SelectableTextAlign, &bb);
7530
7531#ifdef IMGUI_DEBUG_BOXSELECT
7532 if (g.BoxSelectState.UnclipMode) { GetForegroundDrawList()->AddText(pos, IM_COL32(255,255,0,200), label, label_end); }
7533#endif
7534
7535 // Automatically close popups
7536 if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7537 CloseCurrentPopup();
7538
7539 if (disabled_item && !disabled_global)
7540 EndDisabled();
7541
7542 // Selectable() always returns a pressed state!
7543 // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7544 // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7545 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7546 return pressed; //-V1020
7547}
7548
7549bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7550{
7551 if (Selectable(label, *p_selected, flags, size_arg))
7552 {
7553 *p_selected = !*p_selected;
7554 return true;
7555 }
7556 return false;
7557}
7558
7559
7560//-------------------------------------------------------------------------
7561// [SECTION] Widgets: Typing-Select support
7562//-------------------------------------------------------------------------
7563
7564// [Experimental] Currently not exposed in public API.
7565// Consume character inputs and return search request, if any.
7566// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7567// if (ImGui::IsWindowFocused(...))
7568// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7569// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7570// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7571ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7572{
7573 ImGuiContext& g = *GImGui;
7574 ImGuiTypingSelectState* data = &g.TypingSelectState;
7575 ImGuiTypingSelectRequest* out_request = &data->Request;
7576
7577 // Clear buffer
7578 const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7579 const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7580 if (data->SearchBuffer[0] != 0)
7581 {
7582 bool clear_buffer = false;
7583 clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7584 clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7585 clear_buffer |= g.NavAnyRequest;
7586 clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7587 clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7588 clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7589 //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7590 if (clear_buffer)
7591 data->Clear();
7592 }
7593
7594 // Append to buffer
7595 const int buffer_max_len = IM_COUNTOF(data->SearchBuffer) - 1;
7596 int buffer_len = (int)ImStrlen(data->SearchBuffer);
7597 bool select_request = false;
7598 for (ImWchar w : g.IO.InputQueueCharacters)
7599 {
7600 const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7601 if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7602 continue;
7603 char w_buf[5];
7604 ImTextCharToUtf8(w_buf, (unsigned int)w);
7605 if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7606 {
7607 select_request = true; // Same character: don't need to append to buffer.
7608 continue;
7609 }
7610 if (data->SingleCharModeLock)
7611 {
7612 data->Clear(); // Different character: clear
7613 buffer_len = 0;
7614 }
7615 memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7616 buffer_len += w_len;
7617 select_request = true;
7618 }
7619 g.IO.InputQueueCharacters.resize(0);
7620
7621 // Handle backspace
7622 if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7623 {
7624 char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7625 *p = 0;
7626 buffer_len = (int)(p - data->SearchBuffer);
7627 }
7628
7629 // Return request if any
7630 if (buffer_len == 0)
7631 return NULL;
7632 if (select_request)
7633 {
7634 data->FocusScope = g.NavFocusScopeId;
7635 data->LastRequestFrame = g.FrameCount;
7636 data->LastRequestTime = (float)g.Time;
7637 }
7638 out_request->Flags = flags;
7639 out_request->SearchBufferLen = buffer_len;
7640 out_request->SearchBuffer = data->SearchBuffer;
7641 out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7642 out_request->SingleCharMode = false;
7643 out_request->SingleCharSize = 0;
7644
7645 // Calculate if buffer contains the same character repeated.
7646 // - This can be used to implement a special search mode on first character.
7647 // - Performed on UTF-8 codepoint for correctness.
7648 // - SingleCharMode is always set for first input character, because it usually leads to a "next".
7649 if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7650 {
7651 const char* buf_begin = out_request->SearchBuffer;
7652 const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7653 const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7654 const char* p = buf_begin + c0_len;
7655 for (; p < buf_end; p += c0_len)
7656 if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7657 break;
7658 const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7659 out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7660 out_request->SingleCharSize = (ImS8)c0_len;
7661 data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode.
7662 }
7663
7664 return out_request;
7665}
7666
7667static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7668{
7669 int match_len = 0;
7670 while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7671 match_len++;
7672 return match_len;
7673}
7674
7675// Default handler for finding a result for typing-select. You may implement your own.
7676// You might want to display a tooltip to visualize the current request SearchBuffer
7677// When SingleCharMode is set:
7678// - it is better to NOT display a tooltip of other on-screen display indicator.
7679// - the index of the currently focused item is required.
7680// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7681int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7682{
7683 if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7684 return -1;
7685 int idx = -1;
7686 if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7687 idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7688 else
7689 idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7690 if (idx != -1)
7691 SetNavCursorVisibleAfterMove();
7692 return idx;
7693}
7694
7695// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7696int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7697{
7698 // FIXME: Assume selection user data is index. Would be extremely practical.
7699 //if (nav_item_idx == -1)
7700 // nav_item_idx = (int)g.NavLastValidSelectionUserData;
7701
7702 int first_match_idx = -1;
7703 bool return_next_match = false;
7704 for (int idx = 0; idx < items_count; idx++)
7705 {
7706 const char* item_name = get_item_name_func(user_data, idx);
7707 if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7708 continue;
7709 if (return_next_match) // Return next matching item after current item.
7710 return idx;
7711 if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7712 return idx;
7713 if (first_match_idx == -1) // Record first match for wrapping.
7714 first_match_idx = idx;
7715 if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7716 return_next_match = true;
7717 }
7718 return first_match_idx; // First result
7719}
7720
7721int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7722{
7723 int longest_match_idx = -1;
7724 int longest_match_len = 0;
7725 for (int idx = 0; idx < items_count; idx++)
7726 {
7727 const char* item_name = get_item_name_func(user_data, idx);
7728 const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7729 if (match_len <= longest_match_len)
7730 continue;
7731 longest_match_idx = idx;
7732 longest_match_len = match_len;
7733 if (match_len == req->SearchBufferLen)
7734 break;
7735 }
7736 return longest_match_idx;
7737}
7738
7739void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7740{
7741#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7742 Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7743 Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7744 Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7745#else
7746 IM_UNUSED(data);
7747#endif
7748}
7749
7750//-------------------------------------------------------------------------
7751// [SECTION] Widgets: Box-Select support
7752// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7753//-------------------------------------------------------------------------
7754// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7755//-------------------------------------------------------------------------
7756// - BoxSelectPreStartDrag() [Internal]
7757// - BoxSelectActivateDrag() [Internal]
7758// - BoxSelectDeactivateDrag() [Internal]
7759// - BoxSelectScrollWithMouseDrag() [Internal]
7760// - BeginBoxSelect() [Internal]
7761// - EndBoxSelect() [Internal]
7762//-------------------------------------------------------------------------
7763
7764// Call on the initial click.
7765static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7766{
7767 ImGuiContext& g = *GImGui;
7768 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7769 bs->ID = id;
7770 bs->IsStarting = true; // Consider starting box-select.
7771 bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7772 bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7773 bs->KeyMods = g.IO.KeyMods;
7774 bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7775 bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7776}
7777
7778static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7779{
7780 ImGuiContext& g = *GImGui;
7781 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7782 bs->IsActive = true;
7783 bs->Window = window;
7784 bs->IsStarting = false;
7785 ImGui::SetActiveID(bs->ID, window);
7786 ImGui::SetActiveIdUsingAllKeyboardKeys();
7787 if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7788 bs->RequestClear = true;
7789}
7790
7791static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7792{
7793 ImGuiContext& g = *GImGui;
7794 bs->IsActive = bs->IsStarting = false;
7795 if (g.ActiveId == bs->ID)
7796 {
7797 IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7798 ImGui::ClearActiveID();
7799 }
7800 bs->ID = 0;
7801}
7802
7803static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7804{
7805 ImGuiContext& g = *GImGui;
7806 IM_ASSERT(bs->Window == window);
7807 for (int n = 0; n < 2; n++) // each axis
7808 {
7809 const float mouse_pos = g.IO.MousePos[n];
7810 const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f;
7811 const float scroll_curr = window->Scroll[n];
7812 if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7813 continue;
7814
7815 const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7816 const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7817 bs->ScrollAccum[n] += scroll_step;
7818
7819 // Accumulate into a stored value so we can handle high-framerate
7820 const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7821 if (scroll_step_i == 0.0f)
7822 continue;
7823 if (n == 0)
7824 ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7825 else
7826 ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7827 bs->ScrollAccum[n] -= scroll_step_i;
7828 }
7829}
7830
7831bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7832{
7833 ImGuiContext& g = *GImGui;
7834 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7835 KeepAliveID(box_select_id);
7836 if (bs->ID != box_select_id)
7837 return false;
7838
7839 // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7840 bs->UnclipMode = false;
7841 bs->RequestClear = false;
7842 if (bs->IsStarting && IsMouseDragPastThreshold(0))
7843 BoxSelectActivateDrag(bs, window);
7844 else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7845 BoxSelectDeactivateDrag(bs);
7846 if (!bs->IsActive)
7847 return false;
7848
7849 // Current frame absolute prev/current rectangles are used to toggle selection.
7850 // They are derived from positions relative to scrolling space, so "previous" rectangle is reprojected for current frame coordinates.
7851 ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7852 ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7853 ImVec2 curr_end_pos_abs = g.IO.MousePos;
7854 if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7855 curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7856 bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7857 bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7858 bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7859 bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7860 //IMGUI_DEBUG_LOG("StartPosRel (%.2f,%.2f) EndPosRel (%.2f,%.2f) -> (%.2f,%.2f)\n", bs->StartPosRel.x, bs->StartPosRel.y, bs->EndPosRel.x, bs->EndPosRel.y, WindowPosAbsToRel(window, g.IO.MousePos).x, WindowPosAbsToRel(window, g.IO.MousePos).y);
7861
7862 // Box-select 2D mode detects change of the rectangle.
7863 // Storing unclip rects which will be tested by widgets supporting box-select. Always update rectangles when active (even if we don't use them).
7864 // To facilitate understanding this: enable IMGUI_DEBUG_BOXSELECT and visualize all geometry.
7865 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7866 {
7867 // For both sides, compute the area differing between Prev and Curr rectangles.
7868 bs->UnclipRects[0] = bs->UnclipRects[1] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX);
7869 for (int side = 0; side < 2; side++)
7870 {
7871 ImVec2 d_min = (side == 0) ? ImMin(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMin(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
7872 ImVec2 d_max = (side == 0) ? ImMax(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectPrev.Min) : ImMax(bs->BoxSelectRectCurr.Max, bs->BoxSelectRectPrev.Max);
7873 if (d_min.x != d_max.x)
7874 {
7875 bs->UnclipRects[0].AddX(d_min.x);
7876 bs->UnclipRects[0].AddX(d_max.x);
7877 }
7878 if (d_min.y != d_max.y)
7879 {
7880 bs->UnclipRects[1].AddY(d_min.y);
7881 bs->UnclipRects[1].AddY(d_max.y);
7882 }
7883 }
7884
7885 ImRect box_select_intersection = bs->BoxSelectRectPrev;
7886 box_select_intersection.Add(bs->BoxSelectRectCurr);
7887 if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7888 if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7889 {
7890 bs->UnclipRects[0].AddY(box_select_intersection.Min.y);
7891 bs->UnclipRects[0].AddY(box_select_intersection.Max.y);
7892 }
7893 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7894 if (bs->BoxSelectRectPrev.Min.y != bs->BoxSelectRectCurr.Min.y || bs->BoxSelectRectPrev.Max.y != bs->BoxSelectRectCurr.Max.y)
7895 {
7896 bs->UnclipRects[1].AddX(box_select_intersection.Min.x);
7897 bs->UnclipRects[1].AddX(box_select_intersection.Max.x);
7898 }
7899
7900 // Merge both rectangles into one.
7901 // FIXME-OPT: When UnclipRect.Area() is much larger than the sum of UnclipRects[0]/[1] Areas, widgets should
7902 // ideally first use UnclipRect as a first coarse cull layer + the individual ones as a second validation.
7903 bs->UnclipRect = bs->UnclipRects[0];
7904 bs->UnclipRect.Add(bs->UnclipRects[1]);
7905 if (!bs->UnclipRect.IsInverted() && (!window->ClipRect.Contains(bs->UnclipRect.Min) || !window->ClipRect.Contains(bs->UnclipRect.Max))) // !! Don't use Contains(ImRect)
7906 bs->UnclipMode = true;
7907 if (bs->UnclipMode && g.CurrentTable != NULL)
7908 TableApplyExternalUnclipRect(g.CurrentTable, bs->UnclipRect); // No need submitting both
7909 }
7910
7911#ifdef IMGUI_DEBUG_BOXSELECT
7912 //GetForegroundDrawList()->AddRect(scope_rect.Min, scope_rect.Max, IM_COL32(0, 255, 0, 200), 0.0f, 0, 4.0f);
7913 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7914 //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7915 if (ms_flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7916 {
7917 for (ImRect& unclip_r : bs->UnclipRects)
7918 if (!unclip_r.IsInverted())
7919 GetForegroundDrawList()->AddRect(unclip_r.Min, unclip_r.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 4.0f);
7920 GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, bs->UnclipMode ? IM_COL32(255, 255, 0, 200) : IM_COL32(255, 0, 0, 200), 0.0f, 0, 2.0f);
7921 }
7922#endif
7923 return true;
7924}
7925
7926void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7927{
7928 ImGuiContext& g = *GImGui;
7929 ImGuiWindow* window = g.CurrentWindow;
7930 ImGuiBoxSelectState* bs = &g.BoxSelectState;
7931 IM_ASSERT(bs->IsActive);
7932 bs->UnclipMode = false;
7933
7934 // Render selection rectangle
7935 bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7936 ImRect box_select_r = bs->BoxSelectRectCurr;
7937 box_select_r.ClipWith(scope_rect);
7938 ImGuiWindow* draw_window = FindFrontMostVisibleChildWindow(window);
7939 draw_window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7940 draw_window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
7941
7942 // Scroll
7943 const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7944 if (enable_scroll)
7945 {
7946 ImRect scroll_r = scope_rect;
7947 scroll_r.Expand(-g.FontSize);
7948 //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7949 if (!scroll_r.Contains(g.IO.MousePos))
7950 BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7951 }
7952}
7953
7954//-------------------------------------------------------------------------
7955// [SECTION] Widgets: Multi-Select support
7956//-------------------------------------------------------------------------
7957// - DebugLogMultiSelectRequests() [Internal]
7958// - CalcScopeRect() [Internal]
7959// - BeginMultiSelect()
7960// - EndMultiSelect()
7961// - SetNextItemSelectionUserData()
7962// - MultiSelectItemHeader() [Internal]
7963// - MultiSelectItemFooter() [Internal]
7964// - DebugNodeMultiSelectState() [Internal]
7965//-------------------------------------------------------------------------
7966
7967static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7968{
7969 ImGuiContext& g = *GImGui;
7970 IM_UNUSED(function);
7971 for (const ImGuiSelectionRequest& req : io->Requests)
7972 {
7973 if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7974 if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection);
7975 }
7976}
7977
7978static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7979{
7980 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7981 {
7982 // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7983 // This probably doesn't work inside a table as there are ample ambiguities related to exact time of calling BeginMultiSelect()/EndMultiSelect().
7984 return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
7985 }
7986 else
7987 {
7988 //// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7989 ImRect scope_rect = window->InnerClipRect;
7990 //if (g.CurrentTable != NULL)
7991 // scope_rect = g.CurrentTable->HostClipRect;
7992
7993 // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7994 scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
7995 return scope_rect;
7996 }
7997}
7998
7999// Return ImGuiMultiSelectIO structure.
8000// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
8001// Passing 'selection_size' and 'items_count' parameters is currently optional.
8002// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
8003// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
8004// - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently).
8005// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
8006// - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically.
8007ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
8008{
8009 ImGuiContext& g = *GImGui;
8010 ImGuiWindow* window = g.CurrentWindow;
8011
8012 if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
8013 g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
8014 ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
8015 IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
8016 g.CurrentMultiSelect = ms;
8017 if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
8018 flags |= ImGuiMultiSelectFlags_ScopeWindow;
8019 if (flags & ImGuiMultiSelectFlags_SingleSelect)
8020 flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
8021 if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
8022 flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
8023
8024 // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
8025 // They should perhaps be stacked properly?
8026 if (ImGuiTable* table = g.CurrentTable)
8027 {
8028 if (!table->IsLayoutLocked)
8029 TableUpdateLayout(table);
8030 else if (table->CurrentColumn != -1)
8031 TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
8032 }
8033
8034 // FIXME: BeginFocusScope()
8035 const ImGuiID id = window->IDStack.back();
8036 ms->Clear();
8037 ms->FocusScopeId = id;
8038 ms->Flags = flags;
8039 ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
8040 ms->ScopeRectMin = window->DC.CursorPos;
8041 if (flags & ImGuiMultiSelectFlags_ScopeRect)
8042 window->DC.CursorMaxPos = ms->ScopeRectMin; // CalcScopeRect() for ImGuiMultiSelectFlags_ScopeRect will measure in EndMultiSelect().
8043 PushFocusScope(ms->FocusScopeId);
8044 ms->IsFocused = IsInNavFocusRoute(g.CurrentFocusScopeId);
8045 if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
8046 window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
8047
8048 // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
8049 ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
8050 if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
8051 ms->KeyMods &= ~ImGuiMod_Shift;
8052
8053 // Bind storage
8054 ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
8055 storage->ID = id;
8056 storage->LastFrameActive = g.FrameCount;
8057 storage->LastSelectionSize = selection_size;
8058 storage->Window = window;
8059 ms->Storage = storage;
8060
8061 // Output to user
8062 ms->IO.Requests.resize(0);
8063 ms->IO.RangeSrcItem = storage->RangeSrcItem;
8064 ms->IO.NavIdItem = storage->NavIdItem;
8065 ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
8066 ms->IO.ItemsCount = items_count;
8067
8068 // Clear when using Navigation to move within the scope
8069 // (we compare FocusScopeId so it possible to use multiple selections inside a same window)
8070 bool request_clear = false;
8071 bool request_select_all = false;
8072 if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
8073 {
8074 if (ms->KeyMods & ImGuiMod_Shift)
8075 ms->IsKeyboardSetRange = true;
8076 if (ms->IsKeyboardSetRange)
8077 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
8078 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
8079 request_clear = true;
8080 }
8081 else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
8082 {
8083 // Also clear on leaving scope (may be optional?)
8084 if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
8085 request_clear = true;
8086 }
8087
8088 // Box-select handling: update active state.
8089 ImGuiBoxSelectState* bs = &g.BoxSelectState;
8090 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8091 {
8092 ms->BoxSelectId = GetID("##BoxSelect");
8093 if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
8094 request_clear |= bs->RequestClear;
8095 }
8096
8097 if (ms->IsFocused)
8098 {
8099 // Shortcut: Clear selection (Escape)
8100 // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
8101 // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
8102 if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
8103 {
8104 if (selection_size != 0 || bs->IsActive)
8105 if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
8106 {
8107 request_clear = true;
8108 if (bs->IsActive)
8109 BoxSelectDeactivateDrag(bs);
8110 }
8111 }
8112
8113 // Shortcut: Select all (Ctrl+A)
8114 if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
8115 if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
8116 request_select_all = true;
8117 }
8118
8119 if (request_clear || request_select_all)
8120 {
8121 MultiSelectAddSetAll(ms, request_select_all);
8122 if (!request_select_all)
8123 storage->LastSelectionSize = 0;
8124 }
8125 ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
8126 //ms->PrevSubmittedItem = ImGuiSelectionUserData_Invalid;
8127
8128 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
8129 DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
8130
8131 return &ms->IO;
8132}
8133
8134// Return updated ImGuiMultiSelectIO structure.
8135// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
8136ImGuiMultiSelectIO* ImGui::EndMultiSelect()
8137{
8138 ImGuiContext& g = *GImGui;
8139 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8140 ImGuiMultiSelectState* storage = ms->Storage;
8141 ImGuiWindow* window = g.CurrentWindow;
8142 IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
8143 IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
8144 IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
8145
8146 ImRect scope_rect = CalcScopeRect(ms, window);
8147 if (ms->IsFocused)
8148 {
8149 // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
8150 if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at beginning of the scope (see tests for easy failure)
8151 {
8152 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
8153 storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
8154 }
8155 if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
8156 {
8157 IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
8158 storage->NavIdItem = ImGuiSelectionUserData_Invalid;
8159 storage->NavIdSelected = -1;
8160 }
8161
8162 if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
8163 EndBoxSelect(scope_rect, ms->Flags);
8164 }
8165
8166 if (ms->IsEndIO == false)
8167 ms->IO.Requests.resize(0);
8168
8169 // Clear selection when clicking void?
8170 // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
8171 // The InnerRect test is necessary for non-child/decorated windows.
8172 bool scope_hovered = window->InnerRect.Contains(g.IO.MousePos) && IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
8173 if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
8174 scope_hovered &= scope_rect.Contains(g.IO.MousePos);
8175 if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
8176 {
8177 if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8178 {
8179 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
8180 {
8181 BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
8182 FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
8183 SetHoveredID(ms->BoxSelectId);
8184 if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
8185 SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
8186 }
8187 }
8188
8189 if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
8190 if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
8191 MultiSelectAddSetAll(ms, false);
8192 }
8193
8194 // Courtesy nav wrapping helper flag
8195 if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
8196 {
8197 IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
8198 NavMoveRequestTryWrapping(GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
8199 }
8200
8201 // Unwind
8202 if (ImGuiTable* table = g.CurrentTable)
8203 if (table->IsInsideRow)
8204 TableEndRow(table);
8205 window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
8206 PopFocusScope();
8207
8208 if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
8209 DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
8210
8211 ms->FocusScopeId = 0;
8212 ms->Flags = ImGuiMultiSelectFlags_None;
8213 g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
8214
8215 return &ms->IO;
8216}
8217
8218void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
8219{
8220 // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
8221 // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
8222 ImGuiContext& g = *GImGui;
8223 g.NextItemData.SelectionUserData = selection_user_data;
8224 g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
8225
8226 if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
8227 {
8228 // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
8229 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
8230 if (ms->IO.RangeSrcItem == selection_user_data)
8231 ms->RangeSrcPassedBy = true;
8232 //ms->PrevSubmittedItem = ms->CurrSubmittedItem; // Can't rely on previous g.NextItemData.SelectionUserData because NextItemData is not restored on nested multi-select.
8233 //ms->CurrSubmittedItem = selection_user_data;
8234 }
8235 else
8236 {
8237 g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
8238 }
8239}
8240
8241// In charge of:
8242// - Applying SetAll for submitted items.
8243// - Applying SetRange for submitted items and record end points.
8244// - Altering button behavior flags to facilitate use with drag and drop.
8245void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
8246{
8247 ImGuiContext& g = *GImGui;
8248 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8249
8250 bool selected = *p_selected;
8251 if (ms->IsFocused)
8252 {
8253 ImGuiMultiSelectState* storage = ms->Storage;
8254 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8255 IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
8256
8257 // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
8258 // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
8259 // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
8260 if (ms->LoopRequestSetAll != -1)
8261 selected = (ms->LoopRequestSetAll == 1);
8262
8263 // When using Shift+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection)
8264 // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
8265 if (ms->IsKeyboardSetRange)
8266 {
8267 IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
8268 const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
8269 if (is_range_dst)
8270 ms->RangeDstPassedBy = true;
8271 if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
8272 {
8273 storage->RangeSrcItem = item_data;
8274 storage->RangeSelected = selected ? 1 : 0;
8275 }
8276 const bool is_range_src = storage->RangeSrcItem == item_data;
8277 if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
8278 {
8279 // Apply range-select value to visible items
8280 IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
8281 selected = (storage->RangeSelected != 0);
8282 }
8283 else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8284 {
8285 // Clear other items
8286 selected = false;
8287 }
8288 }
8289 *p_selected = selected;
8290 }
8291
8292 // Alter button behavior flags
8293 // To handle drag and drop of multiple items we need to avoid clearing selection on click.
8294 // Enabling this test makes actions using Ctrl+Shift delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items.
8295 if (p_button_flags != NULL)
8296 {
8297 ImGuiButtonFlags button_flags = *p_button_flags;
8298 button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
8299 button_flags &= ~(ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease);
8300 if (ms->Flags & ImGuiMultiSelectFlags_SelectOnClickAlways)
8301 button_flags |= ImGuiButtonFlags_PressedOnClick;
8302 else if (ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)
8303 button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
8304 else // ImGuiMultiSelectFlags_SelectOnAuto
8305 button_flags |= (!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnClickRelease;
8306 *p_button_flags = button_flags;
8307 }
8308}
8309
8310// In charge of:
8311// - Auto-select on navigation.
8312// - Box-select toggle handling.
8313// - Right-click handling.
8314// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
8315// - Record current selection state for RangeSrc
8316// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
8317void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed, ImGuiMultiSelectFlags extra_flags)
8318{
8319 ImGuiContext& g = *GImGui;
8320 ImGuiWindow* window = g.CurrentWindow;
8321
8322 bool selected = *p_selected;
8323 bool pressed = *p_pressed;
8324 ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8325 ImGuiMultiSelectState* storage = ms->Storage;
8326 if (pressed)
8327 ms->IsFocused = true;
8328
8329 bool hovered = false;
8330 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
8331 hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8332 if (!ms->IsFocused && !hovered)
8333 return;
8334
8335 ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8336
8337 ImGuiMultiSelectFlags flags = ms->Flags | extra_flags;
8338 const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
8339 bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
8340 bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
8341
8342 bool apply_to_range_src = false;
8343
8344 if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8345 apply_to_range_src = true;
8346 if (ms->IsEndIO == false)
8347 {
8348 ms->IO.Requests.resize(0);
8349 ms->IsEndIO = true;
8350 }
8351
8352 // Auto-select as you navigate a list
8353 if (g.NavJustMovedToId == id)
8354 {
8355 if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8356 {
8357 if (is_ctrl && is_shift)
8358 pressed = true;
8359 else if (!is_ctrl)
8360 selected = pressed = true;
8361 }
8362 else
8363 {
8364 // With NoAutoSelect, using Shift+keyboard performs a write/copy
8365 if (is_shift)
8366 pressed = true;
8367 else if (!is_ctrl)
8368 apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
8369 }
8370 }
8371
8372 if (apply_to_range_src)
8373 {
8374 storage->RangeSrcItem = item_data;
8375 storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
8376 }
8377
8378 // Box-select toggle handling
8379 if (ms->BoxSelectId != 0)
8380 if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
8381 {
8382 ImRect item_rect = g.LastItemData.Rect;
8383 if (!window->DC.NavIsScrollPushableX) // FIXME: Rename to be more generic.
8384 if (ImGuiTable* table = g.CurrentTable)
8385 if (table->CurrentColumn != -1)
8386 {
8387 // FIXME: We cannot solely use current ClipRect as it includes HostClipRect.
8388 // However we account for ClipRect being larger than current column (e.g. when using SpanAllColumns)
8389 // A more generic version would be nice, but window->WorkRect.Min/Max exclude CellPadding. (#7994, #9383)
8390 ImGuiTableColumn* column = &table->Columns[table->CurrentColumn];
8391 float clip_min_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Min.x : window->ClipRect.Min.x;
8392 float clip_max_x = (g.LastItemData.ItemFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect.Max.x : window->ClipRect.Max.x;
8393 if (clip_min_x != clip_max_x) // When zero sized we expect that bounds have been clamped and thus are unreliable
8394 {
8395 item_rect.Min.x = ImMax(item_rect.Min.x, ImMin(column->MinX, clip_min_x));
8396 item_rect.Max.x = ImMin(item_rect.Max.x, ImMax(column->MaxX, clip_max_x));
8397 }
8398 else
8399 {
8400 item_rect.Min.x = ImMax(item_rect.Min.x, column->MinX);
8401 item_rect.Max.x = ImMin(item_rect.Max.x, column->MaxX);
8402 }
8403 //GetForegroundDrawList()->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255, 0, 255, 255));
8404 }
8405 const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(item_rect);
8406 const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(item_rect);
8407 if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
8408 {
8409 if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
8410 {
8411 pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway)
8412 bs->IsStartedSetNavIdOnce = false;
8413 }
8414 else
8415 {
8416 selected = !selected;
8417 MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
8418#ifdef IMGUI_DEBUG_BOXSELECT
8419 GetForegroundDrawList()->AddRectFilled(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, selected ? IM_COL32(0, 255, 0, 200) : IM_COL32(255, 0, 0, 200));
8420#endif
8421 }
8422 storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
8423 }
8424 }
8425
8426 // Right-click handling.
8427 // FIXME-MULTISELECT: Maybe should be moved to Selectable()? Also see #5816, #8200, #9015
8428 if (hovered && IsMouseClicked(1) && (flags & (ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoSelectOnRightClick)) == 0)
8429 {
8430 if (g.ActiveId != 0 && g.ActiveId != id)
8431 ClearActiveID();
8432 SetFocusID(id, window);
8433 if (!pressed && !selected)
8434 {
8435 pressed = true;
8436 is_ctrl = is_shift = false;
8437 }
8438 }
8439
8440 // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
8441 // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
8442 // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
8443 const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
8444
8445 // Alter selection
8446 if (pressed && (!enter_pressed || !selected))
8447 {
8448 // Box-select
8449 ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
8450 if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8451 if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
8452 BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
8453
8454 //----------------------------------------------------------------------------------------
8455 // ACTION | Begin | Pressed/Activated | End
8456 //----------------------------------------------------------------------------------------
8457 // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
8458 // Keys Navigated: Ctrl | n/a | n/a
8459 // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8460 // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
8461 // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
8462 // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
8463 // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
8464 //----------------------------------------------------------------------------------------
8465 // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
8466 // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
8467 // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8468 // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
8469 //----------------------------------------------------------------------------------------
8470
8471 if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8472 {
8473 bool request_clear = false;
8474 if (is_singleselect)
8475 request_clear = true;
8476 else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
8477 request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
8478 else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
8479 request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
8480 if (request_clear)
8481 MultiSelectAddSetAll(ms, false);
8482 }
8483
8484 int range_direction;
8485 bool range_selected;
8486 if (is_shift && !is_singleselect)
8487 {
8488 //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8489 if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8490 storage->RangeSrcItem = item_data;
8491 if ((flags & ImGuiMultiSelectFlags_CheckboxMode_) == 0)
8492 {
8493 // Shift+Arrow always select
8494 // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8495 range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8496 }
8497 else
8498 {
8499 // Shift+Arrow copy source selection state
8500 // Shift+Click always copy from target selection state
8501 if (ms->IsKeyboardSetRange)
8502 range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8503 else
8504 range_selected = !selected;
8505 }
8506 range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8507 }
8508 else
8509 {
8510 // Ctrl inverts selection, otherwise always select
8511 if ((flags & ImGuiMultiSelectFlags_CheckboxMode_) == 0)
8512 selected = is_ctrl ? !selected : true;
8513 else
8514 selected = !selected;
8515 storage->RangeSrcItem = item_data;
8516 range_selected = selected;
8517 range_direction = +1;
8518 }
8519 MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
8520 }
8521
8522 // Update/store the selection state of the Source item (used by Ctrl+Shift, when Source is unselected we perform a range unselect)
8523 if (storage->RangeSrcItem == item_data)
8524 storage->RangeSelected = selected ? 1 : 0;
8525
8526 // Update/store the selection state of focused item
8527 if (g.NavId == id)
8528 {
8529 storage->NavIdItem = item_data;
8530 storage->NavIdSelected = selected ? 1 : 0;
8531 }
8532 if (storage->NavIdItem == item_data)
8533 ms->NavIdPassedBy = true;
8534
8535 *p_selected = selected;
8536 *p_pressed = pressed;
8537}
8538
8539void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8540{
8541 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8542 ms->IO.Requests.resize(0); // Can always clear previous requests
8543 ms->IO.Requests.push_back(req); // Add new request
8544}
8545
8546void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8547{
8548 // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8549 // FIXME-OPT: Disabled on 2026/04/09 as this would break with any form of coarse clipping that we don't know about (e.g. TableNextColumn() return value).
8550 // The low-hanging fruit would be to know that ImGuiSelectionUserData are sequential indices, in which case we can trivially compare PrevSubmittedItem + RangeDir == FirstItem.
8551 // User can always perform this merge if required.
8552#if 0
8553 if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8554 {
8555 ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8556 if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->PrevSubmittedItem && prev->Selected == selected)
8557 {
8558 prev->RangeLastItem = last_item;
8559 return;
8560 }
8561 }
8562#endif
8563
8564 ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
8565 ms->IO.Requests.push_back(req); // Add new request
8566}
8567
8568void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8569{
8570#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8571 const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8572 if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8573 bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*");
8574 if (!is_active) { PopStyleColor(); }
8575 if (!open)
8576 return;
8577 Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8578 Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8579 Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8580 TreePop();
8581#else
8582 IM_UNUSED(storage);
8583#endif
8584}
8585
8586//-------------------------------------------------------------------------
8587// [SECTION] Widgets: Multi-Select helpers
8588//-------------------------------------------------------------------------
8589// - ImGuiSelectionBasicStorage
8590// - ImGuiSelectionExternalStorage
8591//-------------------------------------------------------------------------
8592
8593ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8594{
8595 Size = 0;
8596 PreserveOrder = false;
8597 UserData = NULL;
8598 AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8599 _SelectionOrder = 1; // Always >0
8600}
8601
8602void ImGuiSelectionBasicStorage::Clear()
8603{
8604 Size = 0;
8605 _SelectionOrder = 1; // Always >0
8606 _Storage.Data.resize(0);
8607}
8608
8609void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8610{
8611 ImSwap(Size, r.Size);
8612 ImSwap(_SelectionOrder, r._SelectionOrder);
8613 _Storage.Data.swap(r._Storage.Data);
8614}
8615
8616bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8617{
8618 return _Storage.GetInt(id, 0) != 0;
8619}
8620
8621static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8622{
8623 int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8624 int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8625 return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8626}
8627
8628// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8629// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8630bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8631{
8632 ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8633 ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8634 if (PreserveOrder && it == NULL && it_end != NULL)
8635 ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8636 if (it == NULL)
8637 it = _Storage.Data.Data;
8638 IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8639 if (it != it_end)
8640 while (it->val_i == 0 && it < it_end)
8641 it++;
8642 const bool has_more = (it != it_end);
8643 *opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8644 *out_id = has_more ? it->key : 0;
8645 if (PreserveOrder && !has_more)
8646 _Storage.BuildSortByKey();
8647 return has_more;
8648}
8649
8650void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8651{
8652 int* p_int = _Storage.GetIntRef(id, 0);
8653 if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8654 else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8655}
8656
8657// Optimized for batch edits (with same value of 'selected')
8658static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8659{
8660 ImGuiStorage* storage = &selection->_Storage;
8661 ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8662 const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8663 if (selected == (is_contained && it->val_i != 0))
8664 return;
8665 if (selected && !is_contained)
8666 storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8667 else if (is_contained)
8668 it->val_i = selected ? selection_order : 0; // Modify in-place.
8669 selection->Size += selected ? +1 : -1;
8670}
8671
8672static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8673{
8674 ImGuiStorage* storage = &selection->_Storage;
8675 if (selected && selection->Size != size_before_amends)
8676 storage->BuildSortByKey(); // When done selecting: sort everything
8677}
8678
8679// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8680// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8681// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8682// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8683// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8684// a lookup in order to have some way to iterate/interpolate between two items.
8685// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8686// and constructing a view index <> object id/ptr data structure anyway.
8687// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8688// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8689// The most simple implementation (using indices everywhere) would look like:
8690// for (ImGuiSelectionRequest& req : ms_io->Requests)
8691// {
8692// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8693// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8694// }
8695void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8696{
8697 // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8698 // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8699 // Other scheme may handle SetAll differently.
8700 IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8701 IM_ASSERT(AdapterIndexToStorageId != NULL);
8702
8703 // This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8704 // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8705 // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8706 // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector<ImGuiID> to reduce bandwidth, but this is a reasonable trade off to reuse code.
8707 // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling
8708 // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.
8709 // FIXME-OPT: For each block of consecutive SetRange request:
8710 // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8711 // - rewrite sorted storage a single time.
8712 for (ImGuiSelectionRequest& req : ms_io->Requests)
8713 {
8714 if (req.Type == ImGuiSelectionRequestType_SetAll)
8715 {
8716 Clear();
8717 if (req.Selected)
8718 {
8719 _Storage.Data.reserve(ms_io->ItemsCount);
8720 const int size_before_amends = _Storage.Data.Size;
8721 for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8722 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8723 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8724 }
8725 }
8726 else if (req.Type == ImGuiSelectionRequestType_SetRange)
8727 {
8728 const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8729 //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected);
8730 if (selection_changes == 1 || (selection_changes < Size / 100))
8731 {
8732 // Multiple sorted insertion + copy likely to be faster.
8733 // Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8734 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8735 SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8736 }
8737 else
8738 {
8739 // Append insertion + single sort likely be faster.
8740 // Use req.RangeDirection to set order field so that Shift+Clicking from 1 to 5 is different than Shift+Clicking from 5 to 1
8741 const int size_before_amends = _Storage.Data.Size;
8742 int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8743 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8744 ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8745 if (req.Selected)
8746 _SelectionOrder += selection_changes;
8747 ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8748 }
8749 }
8750 }
8751}
8752
8753//-------------------------------------------------------------------------
8754
8755ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8756{
8757 UserData = NULL;
8758 AdapterSetItemSelected = NULL;
8759}
8760
8761// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8762// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8763// This makes no assumption about underlying storage.
8764void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8765{
8766 IM_ASSERT(AdapterSetItemSelected);
8767 for (ImGuiSelectionRequest& req : ms_io->Requests)
8768 {
8769 if (req.Type == ImGuiSelectionRequestType_SetAll)
8770 for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8771 AdapterSetItemSelected(this, idx, req.Selected);
8772 if (req.Type == ImGuiSelectionRequestType_SetRange)
8773 for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8774 AdapterSetItemSelected(this, idx, req.Selected);
8775 }
8776}
8777
8778//-------------------------------------------------------------------------
8779// [SECTION] Widgets: ListBox
8780//-------------------------------------------------------------------------
8781// - BeginListBox()
8782// - EndListBox()
8783// - ListBox()
8784//-------------------------------------------------------------------------
8785
8786// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8787// This handle some subtleties with capturing info from the label.
8788// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8789// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8790// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.5f * item_height).
8791bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8792{
8793 ImGuiContext& g = *GImGui;
8794 ImGuiWindow* window = GetCurrentWindow();
8795 if (window->SkipItems)
8796 return false;
8797
8798 const ImGuiStyle& style = g.Style;
8799 const ImGuiID id = GetID(label);
8800 const char* label_end = FindRenderedTextEnd(label);
8801 const ImVec2 label_size = CalcTextSize(label, label_end, false);
8802
8803 // Size default to hold ~7.25 items.
8804 // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8805 ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8806 ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8807 ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8808 ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8809 g.NextItemData.ClearFlags();
8810
8811 if (!IsRectVisible(bb.Min, bb.Max))
8812 {
8813 ItemSize(bb.GetSize(), style.FramePadding.y);
8814 ItemAdd(bb, 0, &frame_bb);
8815 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8816 return false;
8817 }
8818
8819 // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8820 BeginGroup();
8821 if (label_size.x > 0.0f)
8822 {
8823 ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8824 RenderText(label_pos, label, label_end, false);
8825 window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8826 AlignTextToFramePadding();
8827 }
8828
8829 BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8830 return true;
8831}
8832
8833void ImGui::EndListBox()
8834{
8835 ImGuiContext& g = *GImGui;
8836 ImGuiWindow* window = g.CurrentWindow;
8837 IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8838 IM_UNUSED(window);
8839
8840 EndChild();
8841 EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8842}
8843
8844bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8845{
8846 const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8847 return value_changed;
8848}
8849
8850// This is merely a helper around BeginListBox(), EndListBox().
8851// Considering using those directly to submit custom data or store selection differently.
8852bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items)
8853{
8854 ImGuiContext& g = *GImGui;
8855
8856 // Calculate size from "height_in_items"
8857 if (height_in_items < 0)
8858 height_in_items = ImMin(items_count, 7);
8859 float height_in_items_f = height_in_items + 0.25f;
8860 ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8861
8862 if (!BeginListBox(label, size))
8863 return false;
8864
8865 // Assume all items have even height (= 1 line of text). If you need items of different height,
8866 // you can create a custom version of ListBox() in your code without using the clipper.
8867 bool value_changed = false;
8868 ImGuiListClipper clipper;
8869 clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
8870 clipper.IncludeItemByIndex(*current_item);
8871 while (clipper.Step())
8872 for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8873 {
8874 const char* item_text = getter(user_data, i);
8875 if (item_text == NULL)
8876 item_text = "*Unknown item*";
8877
8878 PushID(i);
8879 const bool item_selected = (i == *current_item);
8880 if (Selectable(item_text, item_selected))
8881 {
8882 *current_item = i;
8883 value_changed = true;
8884 }
8885 if (item_selected)
8886 SetItemDefaultFocus();
8887 PopID();
8888 }
8889 EndListBox();
8890
8891 if (value_changed)
8892 MarkItemEdited(g.LastItemData.ID);
8893
8894 return value_changed;
8895}
8896
8897//-------------------------------------------------------------------------
8898// [SECTION] Widgets: PlotLines, PlotHistogram
8899//-------------------------------------------------------------------------
8900// - PlotEx() [Internal]
8901// - PlotLines()
8902// - PlotHistogram()
8903//-------------------------------------------------------------------------
8904// Plot/Graph widgets are not very good.
8905// Consider using ImPlot (https://github.com/epezent/implot) which is much better!
8906//-------------------------------------------------------------------------
8907
8908int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg)
8909{
8910 ImGuiContext& g = *GImGui;
8911 ImGuiWindow* window = GetCurrentWindow();
8912 if (window->SkipItems)
8913 return -1;
8914
8915 const ImGuiStyle& style = g.Style;
8916 const ImGuiID id = window->GetID(label);
8917
8918 const char* label_end = FindRenderedTextEnd(label);
8919 const ImVec2 label_size = CalcTextSize(label, label_end, false);
8920 const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8921
8922 const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8923 const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8924 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));
8925 ItemSize(total_bb, style.FramePadding.y);
8926 if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8927 return -1;
8928 bool hovered;
8929 ButtonBehavior(frame_bb, id, &hovered, NULL);
8930
8931 // Determine scale from values if not specified
8932 if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8933 {
8934 float v_min = FLT_MAX;
8935 float v_max = -FLT_MAX;
8936 for (int i = 0; i < values_count; i++)
8937 {
8938 const float v = values_getter(data, i);
8939 if (v != v) // Ignore NaN values
8940 continue;
8941 v_min = ImMin(v_min, v);
8942 v_max = ImMax(v_max, v);
8943 }
8944 if (scale_min == FLT_MAX)
8945 scale_min = v_min;
8946 if (scale_max == FLT_MAX)
8947 scale_max = v_max;
8948 }
8949
8950 RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8951
8952 const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8953 int idx_hovered = -1;
8954 if (values_count >= values_count_min)
8955 {
8956 int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8957 int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8958
8959 // Tooltip on hover
8960 if (hovered && inner_bb.Contains(g.IO.MousePos))
8961 {
8962 const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.999f);
8963 const int v_idx = (int)(t * item_count);
8964 IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8965
8966 const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8967 const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8968 if (plot_type == ImGuiPlotType_Lines)
8969 SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8970 else if (plot_type == ImGuiPlotType_Histogram)
8971 SetTooltip("%d: %8.4g", v_idx, v0);
8972 idx_hovered = v_idx;
8973 }
8974
8975 const float t_step = 1.0f / (float)res_w;
8976 const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8977
8978 float v0 = values_getter(data, (0 + values_offset) % values_count);
8979 float t0 = 0.0f;
8980 ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8981 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
8982
8983 const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8984 const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8985
8986 for (int n = 0; n < res_w; n++)
8987 {
8988 const float t1 = t0 + t_step;
8989 const int v1_idx = (int)(t0 * item_count + 0.5f);
8990 IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8991 const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8992 const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
8993
8994 // 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.
8995 ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
8996 ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8997 if (plot_type == ImGuiPlotType_Lines)
8998 {
8999 window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
9000 }
9001 else if (plot_type == ImGuiPlotType_Histogram)
9002 {
9003 if (pos1.x >= pos0.x + 2.0f)
9004 pos1.x -= 1.0f;
9005 window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
9006 }
9007
9008 t0 = t1;
9009 tp0 = tp1;
9010 }
9011 }
9012
9013 // Text overlay
9014 if (overlay_text)
9015 RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f));
9016
9017 if (label_size.x > 0.0f)
9018 RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label, label_end, false);
9019
9020 // Return hovered index or -1 if none are hovered.
9021 // 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().
9022 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
9023 return idx_hovered;
9024}
9025
9026struct ImGuiPlotArrayGetterData
9027{
9028 const float* Values;
9029 int Stride;
9030
9031 ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
9032};
9033
9034static float Plot_ArrayGetter(void* data, int idx)
9035{
9036 ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
9037 const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
9038 return v;
9039}
9040
9041void 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)
9042{
9043 ImGuiPlotArrayGetterData data(values, stride);
9044 PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9045}
9046
9047void 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)
9048{
9049 PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9050}
9051
9052// Plot Histogram (the data provided _is_ histogram data. it doesn't compute the histogram of your data)
9053void 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)
9054{
9055 ImGuiPlotArrayGetterData data(values, stride);
9056 PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9057}
9058
9059void 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)
9060{
9061 PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
9062}
9063
9064//-------------------------------------------------------------------------
9065// [SECTION] Widgets: Value helpers
9066// Those is not very useful, legacy API.
9067//-------------------------------------------------------------------------
9068// - Value()
9069//-------------------------------------------------------------------------
9070
9071void ImGui::Value(const char* prefix, bool b)
9072{
9073 Text("%s: %s", prefix, (b ? "true" : "false"));
9074}
9075
9076void ImGui::Value(const char* prefix, int v)
9077{
9078 Text("%s: %d", prefix, v);
9079}
9080
9081void ImGui::Value(const char* prefix, unsigned int v)
9082{
9083 Text("%s: %d", prefix, v);
9084}
9085
9086void ImGui::Value(const char* prefix, float v, const char* float_format)
9087{
9088 if (float_format)
9089 {
9090 char fmt[64];
9091 ImFormatString(fmt, IM_COUNTOF(fmt), "%%s: %s", float_format);
9092 Text(fmt, prefix, v);
9093 }
9094 else
9095 {
9096 Text("%s: %.3f", prefix, v);
9097 }
9098}
9099
9100//-------------------------------------------------------------------------
9101// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
9102//-------------------------------------------------------------------------
9103// - ImGuiMenuColumns [Internal]
9104// - BeginMenuBar()
9105// - EndMenuBar()
9106// - BeginMainMenuBar()
9107// - EndMainMenuBar()
9108// - BeginMenu()
9109// - EndMenu()
9110// - MenuItemEx() [Internal]
9111// - MenuItem()
9112//-------------------------------------------------------------------------
9113
9114// Helpers for internal use
9115void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
9116{
9117 if (window_reappearing)
9118 memset(Widths, 0, sizeof(Widths));
9119 Spacing = (ImU16)spacing;
9120 CalcNextTotalWidth(true);
9121 memset(Widths, 0, sizeof(Widths));
9122 TotalWidth = NextTotalWidth;
9123 NextTotalWidth = 0;
9124}
9125
9126void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
9127{
9128 ImU16 offset = 0;
9129 bool want_spacing = false;
9130 for (int i = 0; i < IM_COUNTOF(Widths); i++)
9131 {
9132 ImU16 width = Widths[i];
9133 if (want_spacing && width > 0)
9134 offset += Spacing;
9135 want_spacing |= (width > 0);
9136 if (update_offsets)
9137 {
9138 if (i == 1) { OffsetLabel = offset; }
9139 if (i == 2) { OffsetShortcut = offset; }
9140 if (i == 3) { OffsetMark = offset; }
9141 }
9142 offset += width;
9143 }
9144 NextTotalWidth = offset;
9145}
9146
9147float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
9148{
9149 Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
9150 Widths[1] = ImMax(Widths[1], (ImU16)w_label);
9151 Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
9152 Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
9153 CalcNextTotalWidth(false);
9154 return (float)ImMax(TotalWidth, NextTotalWidth);
9155}
9156
9157// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
9158// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
9159// 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.
9160// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
9161bool ImGui::BeginMenuBar()
9162{
9163 ImGuiWindow* window = GetCurrentWindow();
9164 if (window->SkipItems)
9165 return false;
9166 if (!(window->Flags & ImGuiWindowFlags_MenuBar))
9167 return false;
9168
9169 IM_ASSERT(!window->DC.MenuBarAppending);
9170 BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
9171 PushID("##MenuBar");
9172
9173 // 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.
9174 // 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.
9175 const float border_top = ImMax(IM_ROUND(window->WindowBorderSize * 0.5f - window->TitleBarHeight), 0.0f);
9176 const float border_half = IM_ROUND(window->WindowBorderSize * 0.5f);
9177 ImRect bar_rect = window->MenuBarRect();
9178 ImRect clip_rect(ImFloor(bar_rect.Min.x + border_half), ImFloor(bar_rect.Min.y + border_top), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, border_half))), ImFloor(bar_rect.Max.y));
9179 clip_rect.ClipWith(window->OuterRectClipped);
9180 PushClipRect(clip_rect.Min, clip_rect.Max, false);
9181
9182 // 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).
9183 window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
9184 window->DC.LayoutType = ImGuiLayoutType_Horizontal;
9185 window->DC.IsSameLine = false;
9186 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
9187 window->DC.MenuBarAppending = true;
9188 AlignTextToFramePadding();
9189 return true;
9190}
9191
9192void ImGui::EndMenuBar()
9193{
9194 ImGuiWindow* window = GetCurrentWindow();
9195 if (window->SkipItems)
9196 return;
9197 ImGuiContext& g = *GImGui;
9198
9199 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9200 IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
9201 IM_ASSERT(window->DC.MenuBarAppending);
9202
9203 // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
9204 if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
9205 {
9206 // Try to find out if the request is for one of our child menu
9207 ImGuiWindow* nav_earliest_child = g.NavWindow;
9208 while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
9209 nav_earliest_child = nav_earliest_child->ParentWindow;
9210 if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
9211 {
9212 // To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
9213 // 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)
9214 const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
9215 IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
9216 FocusWindow(window);
9217 SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
9218 // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
9219 if (g.NavCursorVisible)
9220 {
9221 g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
9222 g.NavCursorHideFrames = 2;
9223 }
9224 g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
9225 NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
9226 }
9227 }
9228 else
9229 {
9230 NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_WrapX);
9231 }
9232
9233 PopClipRect();
9234 PopID();
9235 IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9236 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.
9237
9238 // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
9239 ImGuiGroupData& group_data = g.GroupStack.back();
9240 group_data.EmitItem = false;
9241 ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
9242 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
9243 EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
9244 window->DC.LayoutType = ImGuiLayoutType_Vertical;
9245 window->DC.IsSameLine = false;
9246 window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
9247 window->DC.MenuBarAppending = false;
9248 window->DC.CursorMaxPos = restore_cursor_max_pos;
9249}
9250
9251// Important: calling order matters!
9252// FIXME: Somehow overlapping with docking tech.
9253// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
9254bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
9255{
9256 IM_ASSERT(dir != ImGuiDir_None);
9257
9258 ImGuiWindow* bar_window = FindWindowByName(name);
9259 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
9260 if (bar_window == NULL || bar_window->BeginCount == 0)
9261 {
9262 // Calculate and set window size/position
9263 ImRect avail_rect = viewport->GetBuildWorkRect();
9264 ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
9265 ImVec2 pos = avail_rect.Min;
9266 if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
9267 pos[axis] = avail_rect.Max[axis] - axis_size;
9268 ImVec2 size = avail_rect.GetSize();
9269 size[axis] = axis_size;
9270 SetNextWindowPos(pos);
9271 SetNextWindowSize(size);
9272
9273 // Report our size into work area (for next frame) using actual window size
9274 if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
9275 viewport->BuildWorkInsetMin[axis] += axis_size;
9276 else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
9277 viewport->BuildWorkInsetMax[axis] += axis_size;
9278 }
9279
9280 window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
9281 SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
9282 PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
9283 PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
9284 bool is_open = Begin(name, NULL, window_flags);
9285 PopStyleVar(2);
9286
9287 return is_open;
9288}
9289
9290bool ImGui::BeginMainMenuBar()
9291{
9292 ImGuiContext& g = *GImGui;
9293 ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
9294
9295 // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
9296 SetCurrentViewport(NULL, viewport);
9297
9298 // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
9299 // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
9300 // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
9301 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
9302 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
9303 float height = GetFrameHeight();
9304 bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
9305 g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
9306 if (!is_open)
9307 {
9308 End();
9309 return false;
9310 }
9311
9312 // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
9313 g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
9314 BeginMenuBar();
9315 return is_open;
9316}
9317
9318void ImGui::EndMainMenuBar()
9319{
9320 ImGuiContext& g = *GImGui;
9321 IM_ASSERT_USER_ERROR_RET(g.CurrentWindow->DC.MenuBarAppending, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
9322
9323 EndMenuBar();
9324 g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
9325
9326 // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
9327 // FIXME: With this strategy we won't be able to restore a NULL focus.
9328 if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
9329 FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
9330
9331 End();
9332}
9333
9334static bool IsRootOfOpenMenuSet()
9335{
9336 ImGuiContext& g = *GImGui;
9337 ImGuiWindow* window = g.CurrentWindow;
9338 if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
9339 return false;
9340
9341 // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
9342 // (e.g. inside menu bar vs loose menu items) based on parent ID.
9343 // This would however prevent the use of e.g. PushID() user code submitting menus.
9344 // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
9345 // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
9346 // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
9347 // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
9348 // 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.
9349 // 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
9350 // 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
9351 // it likely won't be a problem anyone runs into.
9352 const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
9353 if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
9354 return false;
9355 return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false);
9356}
9357
9358bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
9359{
9360 ImGuiWindow* window = GetCurrentWindow();
9361 if (window->SkipItems)
9362 return false;
9363
9364 ImGuiContext& g = *GImGui;
9365 ImGuiStyle& style = g.Style;
9366 const ImGuiID id = window->GetID(label);
9367 bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
9368
9369 // 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)
9370 // 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.
9371 ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
9372 if (window->Flags & ImGuiWindowFlags_ChildMenu)
9373 window_flags |= ImGuiWindowFlags_ChildWindow;
9374
9375 // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
9376 // 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.
9377 // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
9378 if (g.MenusIdSubmittedThisFrame.contains(id))
9379 {
9380 if (menu_is_open)
9381 menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
9382 else
9383 g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
9384 return menu_is_open;
9385 }
9386
9387 // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
9388 g.MenusIdSubmittedThisFrame.push_back(id);
9389
9390 const char* label_end = FindRenderedTextEnd(label);
9391 ImVec2 label_size = CalcTextSize(label, label_end, false);
9392
9393 // 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)
9394 // This is only done for items for the menu set and not the full parent window.
9395 const bool menuset_is_open = IsRootOfOpenMenuSet();
9396 if (menuset_is_open)
9397 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9398
9399 // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
9400 // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
9401 // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
9402 ImVec2 popup_pos;
9403 ImVec2 pos = window->DC.CursorPos;
9404 PushID(label);
9405 if (!enabled)
9406 BeginDisabled();
9407
9408 bool pressed;
9409
9410 const float backup_rounding = style.SelectableRounding;
9411 style.SelectableRounding = style.MenuItemRounding;
9412 const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoAutoClosePopups | (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnClick;
9413 ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9414 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9415 {
9416 // Menu inside a horizontal menu bar
9417 // Selectable extend their highlight by half ItemSpacing in each direction.
9418 // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
9419 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9420 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9421 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, pos.y + window->DC.CurrLineTextBaseOffset);
9422 pressed = Selectable("", menu_is_open, selectable_flags, label_size);
9423 LogSetNextTextDecoration("[", "]");
9424 RenderText(text_pos, label, label_end, false);
9425 PopStyleVar();
9426 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
9427 popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), text_pos.y - style.FramePadding.y + window->MenuBarHeight);
9428 }
9429 else
9430 {
9431 // Menu inside a regular/vertical menu
9432 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9433 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9434 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9435 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9436 float min_w = offsets->DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
9437 float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9438 ImVec2 text_pos(window->DC.CursorPos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9439 pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9440 LogSetNextTextDecoration("", ">");
9441 RenderText(ImVec2(text_pos.x + offsets->OffsetLabel, text_pos.y), label, label_end, false);
9442 if (icon_w > 0.0f)
9443 RenderText(ImVec2(text_pos.x + offsets->OffsetIcon, text_pos.y), icon);
9444 RenderArrow(window->DrawList, ImVec2(text_pos.x + offsets->OffsetMark + extra_w + g.FontSize * 0.30f, text_pos.y), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
9445 popup_pos = ImVec2(pos.x, text_pos.y - style.WindowPadding.y);
9446 }
9447 style.SelectableRounding = backup_rounding;
9448 if (!enabled)
9449 EndDisabled();
9450
9451 // Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
9452 // Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
9453 if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
9454 {
9455 ClearActiveID();
9456 SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
9457 }
9458
9459 const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
9460 if (menuset_is_open)
9461 PopItemFlag();
9462
9463 bool want_open = false;
9464 bool want_open_nav_init = false;
9465 bool want_close = false;
9466 if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
9467 {
9468 // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
9469 // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
9470 bool moving_toward_child_menu = false;
9471 ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
9472 ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
9473 if (g.HoveredWindow == window && child_menu_window != NULL)
9474 {
9475 const float ref_unit = g.FontSize; // FIXME-DPI
9476 const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
9477 const ImRect next_window_rect = child_menu_window->Rect();
9478 ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
9479 ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
9480 ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
9481 const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack.
9482 ta.x += child_dir * -0.5f;
9483 tb.x += child_dir * ref_unit;
9484 tc.x += child_dir * ref_unit;
9485 tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus
9486 tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
9487 moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
9488 //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
9489 }
9490
9491 // 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)
9492 // 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.
9493 // (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.)
9494 if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
9495 want_close = true;
9496
9497 // Open
9498 // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
9499 if (!menu_is_open && pressed) // Click/activate to open
9500 want_open = true;
9501 else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
9502 want_open = true;
9503 else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9504 want_open = true;
9505 if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9506 {
9507 want_open = want_open_nav_init = true;
9508 NavMoveRequestCancel();
9509 SetNavCursorVisibleAfterMove();
9510 }
9511 }
9512 else
9513 {
9514 // Menu bar
9515 if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9516 {
9517 want_close = true;
9518 want_open = menu_is_open = false;
9519 }
9520 else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9521 {
9522 want_open = true;
9523 }
9524 else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9525 {
9526 want_open = true;
9527 NavMoveRequestCancel();
9528 }
9529 }
9530
9531 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.. }'
9532 want_close = true;
9533 if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
9534 ClosePopupToLevel(g.BeginPopupStack.Size, true);
9535
9536 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9537 PopID();
9538
9539 if (g.ActiveId == id && want_open)
9540 g.ActiveIdNoClearOnFocusLoss = true;
9541
9542 if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9543 {
9544 // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame.
9545 OpenPopup(label);
9546 }
9547 else if (want_open)
9548 {
9549 menu_is_open = true;
9550 OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9551 }
9552
9553 if (menu_is_open)
9554 {
9555 ImGuiLastItemData last_item_in_parent = g.LastItemData;
9556 SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9557 PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9558 menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open may be 'false' when the popup is completely clipped (e.g. zero size display)
9559 PopStyleVar();
9560 if (menu_is_open)
9561 {
9562 // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9563 // Perform an init request in the case the popup was already open (via a previous mouse hover)
9564 if (want_open && want_open_nav_init && !g.NavInitRequest)
9565 {
9566 FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
9567 NavInitWindow(g.CurrentWindow, false);
9568 }
9569
9570 // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9571 // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9572 g.LastItemData = last_item_in_parent;
9573 if (g.HoveredWindow == window)
9574 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9575 }
9576 }
9577 else
9578 {
9579 g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9580 }
9581
9582 return menu_is_open;
9583}
9584
9585bool ImGui::BeginMenu(const char* label, bool enabled)
9586{
9587 return BeginMenuEx(label, NULL, enabled);
9588}
9589
9590void ImGui::EndMenu()
9591{
9592 // Nav: When a left move request our menu failed, close ourselves.
9593 ImGuiContext& g = *GImGui;
9594 ImGuiWindow* window = g.CurrentWindow;
9595 IM_ASSERT_USER_ERROR_RET((window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu), "Calling EndMenu() in wrong window!");
9596
9597 ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9598 if (window->BeginCount == window->BeginCountPreviousFrame)
9599 if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9600 if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9601 {
9602 ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9603 NavMoveRequestCancel();
9604 }
9605
9606 EndPopup();
9607}
9608
9609bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9610{
9611 ImGuiWindow* window = GetCurrentWindow();
9612 if (window->SkipItems)
9613 return false;
9614
9615 ImGuiContext& g = *GImGui;
9616 ImGuiStyle& style = g.Style;
9617 ImVec2 pos = window->DC.CursorPos;
9618 const char* label_end = FindRenderedTextEnd(label);
9619 ImVec2 label_size = CalcTextSize(label, label_end, false);
9620
9621 // See BeginMenuEx() for comments about this.
9622 const bool menuset_is_open = IsRootOfOpenMenuSet();
9623 if (menuset_is_open)
9624 PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9625
9626 // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9627 // 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.
9628 bool pressed;
9629 PushID(label);
9630 if (!enabled)
9631 BeginDisabled();
9632
9633 // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9634 const float backup_rounding = style.SelectableRounding;
9635 style.SelectableRounding = style.MenuItemRounding;
9636 const ImGuiSelectableFlags selectable_flags = (ImGuiSelectableFlags)ImGuiSelectableFlags_SelectOnRelease | (ImGuiSelectableFlags)ImGuiSelectableFlags_SetNavIdOnHover;
9637 ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9638 if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9639 {
9640 // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9641 // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9642 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9643 ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9644 PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9645 pressed = Selectable("", selected, selectable_flags, ImVec2(label_size.x, 0.0f));
9646 PopStyleVar();
9647 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9648 RenderText(text_pos, label, label_end, false);
9649 window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar().
9650 }
9651 else
9652 {
9653 // Menu item inside a vertical menu
9654 // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9655 // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9656 float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9657 float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9658 float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); // FIXME: Always accounted for, even in a menu with none, because 'selected' has no neutral setting.
9659 float min_w = offsets->DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9660 float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9661 ImVec2 text_pos(pos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9662 pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9663 if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9664 {
9665 RenderText(text_pos + ImVec2(offsets->OffsetLabel, 0.0f), label, label_end, false);
9666 if (icon_w > 0.0f)
9667 RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9668 if (shortcut_w > 0.0f)
9669 {
9670 PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9671 LogSetNextTextDecoration("(", ")");
9672 RenderText(text_pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9673 PopStyleColor();
9674 }
9675 if (selected)
9676 RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9677 }
9678 }
9679 style.SelectableRounding = backup_rounding;
9680
9681 // Once dragged, release ActiveId + key ownership. This is to allow the idiom of mouse down a menu, dragging elsewhere, up on some other MenuItem(). (#8233, #9394)
9682 // Could move logic into lower-level ImGuiButtonFlags_AutoReleaseActiveId + ImGuiButtonFlags_AutoReleaseKeyOwner? Easier once we get rid of the Selectable() middle-man here.
9683 const ImGuiID id = g.LastItemData.ID;
9684 if (g.ActiveId == id && g.HoveredId != id && g.ActiveIdSource == ImGuiInputSource_Mouse && IsMouseDragging(0))
9685 {
9686 ClearActiveID();
9687 SetKeyOwner(ImGuiKey_MouseLeft, ImGuiKeyOwner_NoOwner);
9688 }
9689
9690
9691 IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9692 if (!enabled)
9693 EndDisabled();
9694 PopID();
9695 if (menuset_is_open)
9696 PopItemFlag();
9697
9698 return pressed;
9699}
9700
9701bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9702{
9703 return MenuItemEx(label, NULL, shortcut, selected, enabled);
9704}
9705
9706bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9707{
9708 if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9709 {
9710 if (p_selected)
9711 *p_selected = !*p_selected;
9712 return true;
9713 }
9714 return false;
9715}
9716
9717//-------------------------------------------------------------------------
9718// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9719//-------------------------------------------------------------------------
9720// - BeginTabBar()
9721// - BeginTabBarEx() [Internal]
9722// - EndTabBar()
9723// - TabBarLayout() [Internal]
9724// - TabBarCalcTabID() [Internal]
9725// - TabBarCalcMaxTabWidth() [Internal]
9726// - TabBarFindTabById() [Internal]
9727// - TabBarFindTabByOrder() [Internal]
9728// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
9729// - TabBarGetCurrentTab() [Internal]
9730// - TabBarGetTabName() [Internal]
9731// - TabBarAddTab() [Internal]
9732// - TabBarRemoveTab() [Internal]
9733// - TabBarCloseTab() [Internal]
9734// - TabBarScrollClamp() [Internal]
9735// - TabBarScrollToTab() [Internal]
9736// - TabBarQueueFocus() [Internal]
9737// - TabBarQueueReorder() [Internal]
9738// - TabBarProcessReorderFromMousePos() [Internal]
9739// - TabBarProcessReorder() [Internal]
9740// - TabBarScrollingButtons() [Internal]
9741// - TabBarTabListPopupButton() [Internal]
9742//-------------------------------------------------------------------------
9743
9744struct ImGuiTabBarSection
9745{
9746 int TabCount; // Number of tabs in this section.
9747 float Width; // Sum of width of tabs in this section (after shrinking down)
9748 float WidthAfterShrinkMinWidth;
9749 float Spacing; // Horizontal spacing at the end of the section.
9750
9751 ImGuiTabBarSection() { memset((void*)this, 0, sizeof(*this)); }
9752};
9753
9754namespace ImGui
9755{
9756 static void TabBarLayout(ImGuiTabBar* tab_bar);
9757 static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9758 static float TabBarCalcMaxTabWidth();
9759 static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9760 static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9761 static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9762 static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9763}
9764
9765ImGuiTabBar::ImGuiTabBar()
9766{
9767 memset((void*)this, 0, sizeof(*this));
9768 CurrFrameVisible = PrevFrameVisible = -1;
9769 LastTabItemIdx = -1;
9770}
9771
9772static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9773{
9774 return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9775}
9776
9777static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9778{
9779 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9780 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9781 const int a_section = TabItemGetSectionIdx(a);
9782 const int b_section = TabItemGetSectionIdx(b);
9783 if (a_section != b_section)
9784 return a_section - b_section;
9785 return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9786}
9787
9788static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9789{
9790 const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9791 const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9792 return (int)(a->BeginOrder - b->BeginOrder);
9793}
9794
9795static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9796{
9797 ImGuiContext& g = *GImGui;
9798 return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9799}
9800
9801static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9802{
9803 ImGuiContext& g = *GImGui;
9804 if (g.TabBars.Contains(tab_bar))
9805 return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9806 return ImGuiPtrOrIndex(tab_bar);
9807}
9808
9809ImGuiTabBar* ImGui::TabBarFindByID(ImGuiID id)
9810{
9811 ImGuiContext& g = *GImGui;
9812 return g.TabBars.GetByKey(id);
9813}
9814
9815// Remove TabBar data (currently only used by TestEngine)
9816void ImGui::TabBarRemove(ImGuiTabBar* tab_bar)
9817{
9818 ImGuiContext& g = *GImGui;
9819 g.TabBars.Remove(tab_bar->ID, tab_bar);
9820}
9821
9822bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9823{
9824 ImGuiContext& g = *GImGui;
9825 ImGuiWindow* window = g.CurrentWindow;
9826 if (window->SkipItems)
9827 return false;
9828
9829 ImGuiID id = window->GetID(str_id);
9830 ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9831 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);
9832 tab_bar->ID = id;
9833 tab_bar->SeparatorMinX = tab_bar_bb.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9834 tab_bar->SeparatorMaxX = tab_bar_bb.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9835 //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9836 flags |= ImGuiTabBarFlags_IsFocused;
9837 return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9838}
9839
9840bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9841{
9842 ImGuiContext& g = *GImGui;
9843 ImGuiWindow* window = g.CurrentWindow;
9844 if (window->SkipItems)
9845 return false;
9846
9847 IM_ASSERT(tab_bar->ID != 0);
9848 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // Already done
9849 PushOverrideID(tab_bar->ID);
9850
9851 // Add to stack
9852 g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9853 g.CurrentTabBar = tab_bar;
9854 tab_bar->Window = window;
9855
9856 // Append with multiple BeginTabBar()/EndTabBar() pairs.
9857 tab_bar->BackupCursorPos = window->DC.CursorPos;
9858 if (tab_bar->CurrFrameVisible == g.FrameCount)
9859 {
9860 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9861 tab_bar->BeginCount++;
9862 return true;
9863 }
9864
9865 // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9866 if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9867 if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
9868 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9869 tab_bar->TabsAddedNew = false;
9870
9871 // Flags
9872 if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9873 flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9874
9875 tab_bar->Flags = flags;
9876 tab_bar->BarRect = tab_bar_bb;
9877 tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9878 tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9879 tab_bar->CurrFrameVisible = g.FrameCount;
9880 tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9881 tab_bar->CurrTabsContentsHeight = 0.0f;
9882 tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9883 tab_bar->FramePadding = g.Style.FramePadding;
9884 tab_bar->TabsActiveCount = 0;
9885 tab_bar->LastTabItemIdx = -1;
9886 tab_bar->BeginCount = 1;
9887
9888 // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9889 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9890
9891 // Draw separator
9892 // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9893 const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9894 if (g.Style.TabBarBorderSize > 0.0f)
9895 {
9896 const float y = tab_bar->BarRect.Max.y;
9897 window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9898 }
9899 return true;
9900}
9901
9902void ImGui::EndTabBar()
9903{
9904 ImGuiContext& g = *GImGui;
9905 ImGuiWindow* window = g.CurrentWindow;
9906 if (window->SkipItems)
9907 return;
9908
9909 ImGuiTabBar* tab_bar = g.CurrentTabBar;
9910 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9911
9912 // Fallback in case no TabItem have been submitted
9913 if (tab_bar->WantLayout)
9914 TabBarLayout(tab_bar);
9915
9916 // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9917 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9918 if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9919 {
9920 tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9921 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9922 }
9923 else
9924 {
9925 window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9926 }
9927 if (tab_bar->BeginCount > 1)
9928 window->DC.CursorPos = tab_bar->BackupCursorPos;
9929
9930 tab_bar->LastTabItemIdx = -1;
9931 if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) // Already done
9932 PopID();
9933
9934 g.CurrentTabBarStack.pop_back();
9935 g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9936}
9937
9938// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9939static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9940{
9941 return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9942}
9943
9944// This is called only once a frame before by the first call to ItemTab()
9945// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9946static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9947{
9948 ImGuiContext& g = *GImGui;
9949 tab_bar->WantLayout = false;
9950
9951 // Track selected tab when resizing our parent down
9952 const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
9953 tab_bar->BarRectPrevWidth = tab_bar->BarRect.GetWidth();
9954
9955 // Garbage collect by compacting list
9956 // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9957 int tab_dst_n = 0;
9958 bool need_sort_by_section = false;
9959 ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9960 for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9961 {
9962 ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9963 if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9964 {
9965 // Remove tab
9966 if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9967 if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9968 if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9969 continue;
9970 }
9971 if (tab_dst_n != tab_src_n)
9972 tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9973
9974 tab = &tab_bar->Tabs[tab_dst_n];
9975 tab->IndexDuringLayout = (ImS16)tab_dst_n;
9976
9977 // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9978 int curr_tab_section_n = TabItemGetSectionIdx(tab);
9979 if (tab_dst_n > 0)
9980 {
9981 ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9982 int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
9983 if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9984 need_sort_by_section = true;
9985 if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9986 need_sort_by_section = true;
9987 }
9988
9989 sections[curr_tab_section_n].TabCount++;
9990 tab_dst_n++;
9991 }
9992 if (tab_bar->Tabs.Size != tab_dst_n)
9993 tab_bar->Tabs.resize(tab_dst_n);
9994
9995 if (need_sort_by_section)
9996 ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
9997
9998 // Calculate spacing between sections
9999 const float tab_spacing = g.Style.ItemInnerSpacing.x;
10000 sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? tab_spacing : 0.0f;
10001 sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? tab_spacing : 0.0f;
10002
10003 // Setup next selected tab
10004 ImGuiID scroll_to_tab_id = 0;
10005 if (tab_bar->NextScrollToTabId)
10006 {
10007 scroll_to_tab_id = tab_bar->NextScrollToTabId;
10008 tab_bar->NextScrollToTabId = 0;
10009 }
10010 if (tab_bar->NextSelectedTabId)
10011 {
10012 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
10013 tab_bar->NextSelectedTabId = 0;
10014 scroll_to_tab_id = tab_bar->SelectedTabId;
10015 }
10016
10017 // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
10018 if (tab_bar->ReorderRequestTabId != 0)
10019 {
10020 if (TabBarProcessReorder(tab_bar))
10021 if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
10022 scroll_to_tab_id = tab_bar->ReorderRequestTabId;
10023 tab_bar->ReorderRequestTabId = 0;
10024 }
10025
10026 // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
10027 const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
10028 if (tab_list_popup_button)
10029 if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
10030 scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
10031
10032 // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
10033 // (whereas our tabs are stored as: leading, central, trailing)
10034 int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
10035 g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
10036
10037 // Minimum shrink width
10038 const float shrink_min_width = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed) ? g.Style.TabMinWidthShrink : 1.0f;
10039
10040 // Compute ideal tabs widths + store them into shrink buffer
10041 ImGuiTabItem* most_recently_selected_tab = NULL;
10042 int curr_section_n = -1;
10043 bool found_selected_tab_id = false;
10044 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10045 {
10046 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10047 IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
10048
10049 if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
10050 most_recently_selected_tab = tab;
10051 if (tab->ID == tab_bar->SelectedTabId)
10052 found_selected_tab_id = true;
10053 if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
10054 scroll_to_tab_id = tab->ID;
10055
10056 // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
10057 // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
10058 // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
10059 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10060 const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10061 tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
10062 if ((tab->Flags & ImGuiTabItemFlags_Button) == 0)
10063 tab->ContentWidth = ImMax(tab->ContentWidth, g.Style.TabMinWidthBase);
10064
10065 int section_n = TabItemGetSectionIdx(tab);
10066 ImGuiTabBarSection* section = &sections[section_n];
10067 section->Width += tab->ContentWidth + (section_n == curr_section_n ? tab_spacing : 0.0f);
10068 section->WidthAfterShrinkMinWidth += ImMin(tab->ContentWidth, shrink_min_width) + (section_n == curr_section_n ? tab_spacing : 0.0f);
10069 curr_section_n = section_n;
10070
10071 // Store data so we can build an array sorted by width if we need to shrink tabs down
10072 IM_MSVC_WARNING_SUPPRESS(6385);
10073 ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
10074 shrink_width_item->Index = tab_n;
10075 shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
10076 tab->Width = ImMax(tab->ContentWidth, 1.0f);
10077 }
10078
10079 // Compute total ideal width (used for e.g. auto-resizing a window)
10080 float width_all_tabs_after_min_width_shrink = 0.0f;
10081 tab_bar->WidthAllTabsIdeal = 0.0f;
10082 for (int section_n = 0; section_n < 3; section_n++)
10083 {
10084 tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
10085 width_all_tabs_after_min_width_shrink += sections[section_n].WidthAfterShrinkMinWidth + sections[section_n].Spacing;
10086 }
10087
10088 // Horizontal scrolling buttons
10089 // Important: note that TabBarScrollButtons() will alter BarRect.Max.x.
10090 const bool can_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
10091 const float width_all_tabs_to_use_for_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) ? tab_bar->WidthAllTabs : width_all_tabs_after_min_width_shrink;
10092 tab_bar->ScrollButtonEnabled = ((width_all_tabs_to_use_for_scroll > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && can_scroll);
10093 if (tab_bar->ScrollButtonEnabled)
10094 if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
10095 {
10096 scroll_to_tab_id = scroll_and_select_tab->ID;
10097 if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
10098 tab_bar->SelectedTabId = scroll_to_tab_id;
10099 }
10100 if (scroll_to_tab_id == 0 && scroll_to_selected_tab)
10101 scroll_to_tab_id = tab_bar->SelectedTabId;
10102
10103 // Shrink widths if full tabs don't fit in their allocated space
10104 float section_0_w = sections[0].Width + sections[0].Spacing;
10105 float section_1_w = sections[1].Width + sections[1].Spacing;
10106 float section_2_w = sections[2].Width + sections[2].Spacing;
10107 bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
10108 float width_excess;
10109 if (central_section_is_visible)
10110 width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
10111 else
10112 width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
10113
10114 // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
10115 const bool can_shrink = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyShrink) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
10116 if (width_excess >= 1.0f && (can_shrink || !central_section_is_visible))
10117 {
10118 int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
10119 int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
10120 ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess, shrink_min_width);
10121
10122 // Apply shrunk values into tabs and sections
10123 for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
10124 {
10125 ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
10126 float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
10127 if (shrinked_width < 0.0f)
10128 continue;
10129
10130 shrinked_width = ImMax(1.0f, shrinked_width);
10131 int section_n = TabItemGetSectionIdx(tab);
10132 sections[section_n].Width -= (tab->Width - shrinked_width);
10133 tab->Width = shrinked_width;
10134 }
10135 }
10136
10137 // Layout all active tabs
10138 int section_tab_index = 0;
10139 float tab_offset = 0.0f;
10140 tab_bar->WidthAllTabs = 0.0f;
10141 for (int section_n = 0; section_n < 3; section_n++)
10142 {
10143 ImGuiTabBarSection* section = &sections[section_n];
10144 if (section_n == 2)
10145 tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
10146
10147 for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
10148 {
10149 ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
10150 tab->Offset = tab_offset;
10151 tab->NameOffset = -1;
10152 tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
10153 }
10154 tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
10155 tab_offset += section->Spacing;
10156 section_tab_index += section->TabCount;
10157 }
10158
10159 // Clear name buffers
10160 tab_bar->TabsNames.Buf.resize(0);
10161
10162 // If we have lost the selected tab, select the next most recently active one
10163 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10164 if (found_selected_tab_id == false && !tab_bar_appearing)
10165 tab_bar->SelectedTabId = 0;
10166 if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
10167 scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
10168
10169 // Lock in visible tab
10170 tab_bar->VisibleTabId = tab_bar->SelectedTabId;
10171 tab_bar->VisibleTabWasSubmitted = false;
10172
10173 // CTRL+TAB can override visible tab temporarily
10174 if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
10175 tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
10176
10177 // Apply request requests
10178 if (scroll_to_tab_id != 0)
10179 TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
10180 else if (tab_bar->ScrollButtonEnabled && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
10181 {
10182 const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
10183 const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
10184 if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
10185 {
10186 const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
10187 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
10188 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
10189 }
10190 SetKeyOwner(wheel_key, tab_bar->ID);
10191 }
10192
10193 // Update scrolling
10194 tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
10195 tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
10196 if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
10197 {
10198 // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
10199 // Teleport if we are aiming far off the visible line
10200 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
10201 tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
10202 const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
10203 tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
10204 }
10205 else
10206 {
10207 tab_bar->ScrollingSpeed = 0.0f;
10208 }
10209 tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
10210 tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
10211
10212 // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
10213 ImGuiWindow* window = g.CurrentWindow;
10214 window->DC.CursorPos = tab_bar->BarRect.Min;
10215 ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
10216 window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
10217}
10218
10219// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
10220static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
10221{
10222 if (docked_window != NULL)
10223 {
10224 IM_UNUSED(tab_bar);
10225 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10226 ImGuiID id = docked_window->TabId;
10227 KeepAliveID(id);
10228 return id;
10229 }
10230 else
10231 {
10232 ImGuiWindow* window = GImGui->CurrentWindow;
10233 return window->GetID(label);
10234 }
10235}
10236
10237static float ImGui::TabBarCalcMaxTabWidth()
10238{
10239 ImGuiContext& g = *GImGui;
10240 return g.FontSize * 20.0f;
10241}
10242
10243ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10244{
10245 if (tab_id != 0)
10246 for (int n = 0; n < tab_bar->Tabs.Size; n++)
10247 if (tab_bar->Tabs[n].ID == tab_id)
10248 return &tab_bar->Tabs[n];
10249 return NULL;
10250}
10251
10252// Order = visible order, not submission order! (which is tab->BeginOrder)
10253ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
10254{
10255 if (order < 0 || order >= tab_bar->Tabs.Size)
10256 return NULL;
10257 return &tab_bar->Tabs[order];
10258}
10259
10260// FIXME: See references to #2304 in TODO.txt
10261ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
10262{
10263 ImGuiTabItem* most_recently_selected_tab = NULL;
10264 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10265 {
10266 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10267 if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
10268 if (tab->Window && tab->Window->WasActive)
10269 most_recently_selected_tab = tab;
10270 }
10271 return most_recently_selected_tab;
10272}
10273
10274ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
10275{
10276 if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
10277 return NULL;
10278 return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10279}
10280
10281const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10282{
10283 if (tab->Window)
10284 return tab->Window->Name;
10285 if (tab->NameOffset == -1)
10286 return "N/A";
10287 IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
10288 return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
10289}
10290
10291// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
10292// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
10293void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
10294{
10295 ImGuiContext& g = *GImGui;
10296 IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
10297 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)
10298
10299 if (!window->HasCloseButton)
10300 tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
10301
10302 ImGuiTabItem new_tab;
10303 new_tab.ID = window->TabId;
10304 new_tab.Flags = tab_flags;
10305 new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
10306 if (new_tab.LastFrameVisible == -1)
10307 new_tab.LastFrameVisible = g.FrameCount - 1;
10308 new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
10309 tab_bar->Tabs.push_back(new_tab);
10310}
10311
10312// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
10313void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10314{
10315 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10316 tab_bar->Tabs.erase(tab);
10317 if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
10318 if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
10319 if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
10320}
10321
10322// Called on manual closure attempt
10323void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10324{
10325 if (tab->Flags & ImGuiTabItemFlags_Button)
10326 return; // A button appended with TabItemButton().
10327
10328 if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
10329 {
10330 // This will remove a frame of lag for selecting another tab on closure.
10331 // 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
10332 tab->WantClose = true;
10333 if (tab_bar->VisibleTabId == tab->ID)
10334 {
10335 tab->LastFrameVisible = -1;
10336 tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
10337 }
10338 }
10339 else
10340 {
10341 // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
10342 if (tab_bar->VisibleTabId != tab->ID)
10343 TabBarQueueFocus(tab_bar, tab);
10344 }
10345}
10346
10347static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
10348{
10349 scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
10350 return ImMax(scrolling, 0.0f);
10351}
10352
10353// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
10354static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
10355{
10356 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
10357 if (tab == NULL)
10358 return;
10359 if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
10360 return;
10361
10362 ImGuiContext& g = *GImGui;
10363 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)
10364 int order = TabBarGetTabOrder(tab_bar, tab);
10365
10366 // Scrolling happens only in the central section (leading/trailing sections are not scrolling)
10367 float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
10368
10369 // We make all tabs positions all relative Sections[0].Width to make code simpler
10370 float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
10371 float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
10372 tab_bar->ScrollingTargetDistToVisibility = 0.0f;
10373 if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
10374 {
10375 // Scroll to the left
10376 tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
10377 tab_bar->ScrollingTarget = tab_x1;
10378 }
10379 else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
10380 {
10381 // Scroll to the right
10382 tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
10383 tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
10384 }
10385}
10386
10387void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10388{
10389 tab_bar->NextSelectedTabId = tab->ID;
10390}
10391
10392void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
10393{
10394 IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
10395 ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
10396 tab_bar->NextSelectedTabId = tab_id;
10397}
10398
10399void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
10400{
10401 IM_ASSERT(offset != 0);
10402 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10403 tab_bar->ReorderRequestTabId = tab->ID;
10404 tab_bar->ReorderRequestOffset = (ImS16)offset;
10405}
10406
10407void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
10408{
10409 ImGuiContext& g = *GImGui;
10410 IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10411 if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
10412 return;
10413
10414 const float tab_spacing = g.Style.ItemInnerSpacing.x;
10415 const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10416 const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
10417
10418 // Count number of contiguous tabs we are crossing over
10419 const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
10420 const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
10421 int dst_idx = src_idx;
10422 for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
10423 {
10424 // Reordered tabs must share the same section
10425 const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
10426 if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
10427 break;
10428 if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
10429 break;
10430 dst_idx = i;
10431
10432 // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
10433 const float x1 = bar_offset + dst_tab->Offset - tab_spacing;
10434 const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + tab_spacing;
10435 //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
10436 if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
10437 break;
10438 }
10439
10440 if (dst_idx != src_idx)
10441 TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
10442}
10443
10444bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
10445{
10446 ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
10447 if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
10448 return false;
10449
10450 //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
10451 int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
10452 if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
10453 return false;
10454
10455 // Reordered tabs must share the same section
10456 // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
10457 ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
10458 if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
10459 return false;
10460 if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
10461 return false;
10462
10463 ImGuiTabItem item_tmp = *tab1;
10464 ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
10465 ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
10466 const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
10467 memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
10468 *tab2 = item_tmp;
10469
10470 if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
10471 MarkIniSettingsDirty();
10472 return true;
10473}
10474
10475static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
10476{
10477 ImGuiContext& g = *GImGui;
10478 ImGuiWindow* window = g.CurrentWindow;
10479
10480 const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
10481 const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
10482
10483 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10484 //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));
10485
10486 int select_dir = 0;
10487 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10488 arrow_col.w *= 0.5f;
10489
10490 PushStyleColor(ImGuiCol_Text, arrow_col);
10491 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10492 PushItemFlag(ImGuiItemFlags_ButtonRepeat | ImGuiItemFlags_NoNav, true);
10493 const float backup_repeat_delay = g.IO.KeyRepeatDelay;
10494 const float backup_repeat_rate = g.IO.KeyRepeatRate;
10495 g.IO.KeyRepeatDelay = 0.250f;
10496 g.IO.KeyRepeatRate = 0.200f;
10497 float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
10498 window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
10499 if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10500 select_dir = -1;
10501 window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
10502 if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10503 select_dir = +1;
10504 PopItemFlag();
10505 PopStyleColor(2);
10506 g.IO.KeyRepeatRate = backup_repeat_rate;
10507 g.IO.KeyRepeatDelay = backup_repeat_delay;
10508
10509 ImGuiTabItem* tab_to_scroll_to = NULL;
10510 if (select_dir != 0)
10511 if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
10512 {
10513 int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
10514 int target_order = selected_order + select_dir;
10515
10516 // Skip tab item buttons until another tab item is found or end is reached
10517 while (tab_to_scroll_to == NULL)
10518 {
10519 // If we are at the end of the list, still scroll to make our tab visible
10520 tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
10521
10522 // Cross through buttons
10523 // (even if first/last item is a button, return it so we can update the scroll)
10524 if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
10525 {
10526 target_order += select_dir;
10527 selected_order += select_dir;
10528 tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
10529 }
10530 }
10531 }
10532 window->DC.CursorPos = backup_cursor_pos;
10533 tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
10534
10535 return tab_to_scroll_to;
10536}
10537
10538static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
10539{
10540 ImGuiContext& g = *GImGui;
10541 ImGuiWindow* window = g.CurrentWindow;
10542
10543 // We use g.Style.FramePadding.y to match the square ArrowButton size
10544 const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
10545 const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10546 window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
10547 tab_bar->BarRect.Min.x += tab_list_popup_button_width;
10548
10549 ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10550 arrow_col.w *= 0.5f;
10551 PushStyleColor(ImGuiCol_Text, arrow_col);
10552 PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10553 bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
10554 PopStyleColor(2);
10555
10556 ImGuiTabItem* tab_to_select = NULL;
10557 if (open)
10558 {
10559 for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10560 {
10561 ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10562 if (tab->Flags & ImGuiTabItemFlags_Button)
10563 continue;
10564
10565 const char* tab_name = TabBarGetTabName(tab_bar, tab);
10566 if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
10567 tab_to_select = tab;
10568 }
10569 EndCombo();
10570 }
10571
10572 window->DC.CursorPos = backup_cursor_pos;
10573 return tab_to_select;
10574}
10575
10576//-------------------------------------------------------------------------
10577// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10578//-------------------------------------------------------------------------
10579// - BeginTabItem()
10580// - EndTabItem()
10581// - TabItemButton()
10582// - TabItemEx() [Internal]
10583// - SetTabItemClosed()
10584// - TabItemCalcSize() [Internal]
10585// - TabItemBackground() [Internal]
10586// - TabItemLabelAndCloseButton() [Internal]
10587//-------------------------------------------------------------------------
10588
10589bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10590{
10591 ImGuiContext& g = *GImGui;
10592 ImGuiWindow* window = g.CurrentWindow;
10593 if (window->SkipItems)
10594 return false;
10595
10596 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10597 IM_ASSERT_USER_ERROR_RETV(tab_bar != NULL, false, "Needs to be called between BeginTabBar() and EndTabBar()!");
10598 IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10599
10600 bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10601 if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10602 {
10603 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10604 PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10605 }
10606 return ret;
10607}
10608
10609void ImGui::EndTabItem()
10610{
10611 ImGuiContext& g = *GImGui;
10612 ImGuiWindow* window = g.CurrentWindow;
10613 if (window->SkipItems)
10614 return;
10615
10616 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10617 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10618 IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10619 ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10620 if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10621 PopID();
10622}
10623
10624bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10625{
10626 ImGuiContext& g = *GImGui;
10627 ImGuiWindow* window = g.CurrentWindow;
10628 if (window->SkipItems)
10629 return false;
10630
10631 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10632 IM_ASSERT_USER_ERROR_RETV(tab_bar != NULL, false, "Needs to be called between BeginTabBar() and EndTabBar()!");
10633 return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10634}
10635
10636void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10637{
10638 ImGuiContext& g = *GImGui;
10639 ImGuiWindow* window = g.CurrentWindow;
10640 if (window->SkipItems)
10641 return;
10642
10643 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10644 IM_ASSERT_USER_ERROR_RET(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10645 SetNextItemWidth(width);
10646 TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10647}
10648
10649bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10650{
10651 // Layout whole tab bar if not already done
10652 ImGuiContext& g = *GImGui;
10653 if (tab_bar->WantLayout)
10654 {
10655 ImGuiNextItemData backup_next_item_data = g.NextItemData;
10656 TabBarLayout(tab_bar);
10657 g.NextItemData = backup_next_item_data;
10658 }
10659 ImGuiWindow* window = g.CurrentWindow;
10660 if (window->SkipItems)
10661 return false;
10662
10663 const ImGuiStyle& style = g.Style;
10664 const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10665
10666 // If the user called us with *p_open == false, we early out and don't render.
10667 // 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.
10668 IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10669 if (p_open && !*p_open)
10670 {
10671 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10672 return false;
10673 }
10674
10675 IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10676 IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10677
10678 // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10679 if (flags & ImGuiTabItemFlags_NoCloseButton)
10680 p_open = NULL;
10681 else if (p_open == NULL)
10682 flags |= ImGuiTabItemFlags_NoCloseButton;
10683
10684 // Acquire tab data
10685 ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10686 bool tab_is_new = false;
10687 if (tab == NULL)
10688 {
10689 tab_bar->Tabs.push_back(ImGuiTabItem());
10690 tab = &tab_bar->Tabs.back();
10691 tab->ID = id;
10692 tab_bar->TabsAddedNew = tab_is_new = true;
10693 }
10694 tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10695
10696 // Calculate tab contents size
10697 ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10698 tab->RequestedWidth = -1.0f;
10699 if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10700 size.x = tab->RequestedWidth = g.NextItemData.Width;
10701 if (tab_is_new)
10702 tab->Width = ImMax(1.0f, size.x);
10703 tab->ContentWidth = size.x;
10704 tab->BeginOrder = tab_bar->TabsActiveCount++;
10705
10706 const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10707 const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10708 const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10709 const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10710 const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10711 tab->LastFrameVisible = g.FrameCount;
10712 tab->Flags = flags;
10713 tab->Window = docked_window;
10714
10715 // Append name _WITH_ the zero-terminator
10716 // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
10717 if (docked_window != NULL)
10718 {
10719 IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10720 tab->NameOffset = -1;
10721 }
10722 else
10723 {
10724 tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10725 tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1);
10726 }
10727
10728 // Update selected tab
10729 if (!is_tab_button)
10730 {
10731 if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10732 if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10733 TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10734 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10735 TabBarQueueFocus(tab_bar, tab);
10736 }
10737
10738 // Lock visibility
10739 // (Note: tab_contents_visible != tab_selected... because Ctrl+Tab operations may preview some tabs without selecting them!)
10740 bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10741 if (tab_contents_visible)
10742 tab_bar->VisibleTabWasSubmitted = true;
10743
10744 // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10745 if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
10746 if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10747 tab_contents_visible = true;
10748
10749 // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10750 // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10751 if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10752 {
10753 ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10754 if (is_tab_button)
10755 return false;
10756 return tab_contents_visible;
10757 }
10758
10759 if (tab_bar->SelectedTabId == id)
10760 tab->LastFrameSelected = g.FrameCount;
10761
10762 // Backup current layout position
10763 const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10764
10765 // Layout
10766 const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10767 size.x = tab->Width;
10768 if (is_central_section)
10769 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10770 else
10771 window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10772 ImVec2 pos = window->DC.CursorPos;
10773 ImRect bb(pos, pos + size);
10774
10775 // 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)
10776 const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10777 if (want_clip_rect)
10778 PushClipRect(ImVec2(ImClamp(bb.Min.x, tab_bar->ScrollingRectMinX, tab_bar->ScrollingRectMaxX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10779
10780 ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10781 ItemSize(bb.GetSize(), style.FramePadding.y);
10782 window->DC.CursorMaxPos = backup_cursor_max_pos;
10783
10784 if (!ItemAdd(bb, id))
10785 {
10786 if (want_clip_rect)
10787 PopClipRect();
10788 window->DC.CursorPos = backup_main_cursor_pos;
10789 return tab_contents_visible;
10790 }
10791
10792 // Click to Select a tab
10793 ImGuiButtonFlags button_flags = ((ImGuiButtonFlags)(is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10794 if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
10795 button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10796 bool hovered, held, pressed;
10797 if (flags & ImGuiTabItemFlags_Invisible)
10798 hovered = held = pressed = false;
10799 else
10800 pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10801 if (pressed && !is_tab_button)
10802 TabBarQueueFocus(tab_bar, tab);
10803
10804 // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
10805 // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
10806 if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
10807 g.ActiveIdWindow = docked_window;
10808
10809 // Drag and drop a single floating window node moves it
10810 ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
10811 const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
10812 if (held && single_floating_window_node && IsMouseDragging(0, 0.0f))
10813 {
10814 // Move
10815 StartMouseMovingWindow(docked_window);
10816 }
10817 else if (held && !tab_appearing && IsMouseDragging(0))
10818 {
10819 // Drag and drop: re-order tabs
10820 int drag_dir = 0;
10821 float drag_distance_from_edge_x = 0.0f;
10822 if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
10823 {
10824 // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10825 if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10826 {
10827 drag_dir = -1;
10828 drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
10829 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10830 }
10831 else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10832 {
10833 drag_dir = +1;
10834 drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
10835 TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10836 }
10837 }
10838
10839 // Extract a Dockable window out of it's tab bar
10840 const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
10841 if (can_undock)
10842 {
10843 // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
10844 bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
10845 if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
10846 {
10847 float threshold_base = g.FontSize;
10848 float threshold_x = (threshold_base * 2.2f);
10849 float threshold_y = (threshold_base * 1.5f) + ImClamp((ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, 0.0f, threshold_base * 4.0f);
10850 //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]
10851
10852 float distance_from_edge_y = ImMax(bb.Min.y - g.IO.MousePos.y, g.IO.MousePos.y - bb.Max.y);
10853 if (distance_from_edge_y >= threshold_y)
10854 undocking_tab = true;
10855 if (drag_distance_from_edge_x > threshold_x)
10856 if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
10857 undocking_tab = true;
10858 }
10859
10860 if (undocking_tab)
10861 {
10862 // Undock
10863 // FIXME: refactor to share more code with e.g. StartMouseMovingWindow
10864 DockContextQueueUndockWindow(&g, docked_window);
10865 g.MovingWindow = docked_window;
10866 SetActiveID(g.MovingWindow->MoveId, g.MovingWindow);
10867 g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
10868 g.ActiveIdNoClearOnFocusLoss = true;
10869 SetActiveIdUsingAllKeyboardKeys();
10870 }
10871 }
10872 }
10873
10874#if 0
10875 if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10876 {
10877 // Enlarge tab display when hovering
10878 bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10879 display_draw_list = GetForegroundDrawList(window);
10880 TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10881 }
10882#endif
10883
10884 // Render tab shape
10885 const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10886 if (is_visible)
10887 {
10888 ImDrawList* display_draw_list = window->DrawList;
10889 const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed));
10890 TabItemBackground(display_draw_list, bb, flags, tab_col);
10891 if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10892 {
10893 // Might be moved to TabItemBackground() ?
10894 ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10895 ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10896 ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10897 if (style.TabRounding > 0.0f)
10898 {
10899 float rounding = style.TabRounding;
10900 display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9);
10901 display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11);
10902 display_draw_list->PathStroke(overline_col, style.TabBarOverlineSize);
10903 }
10904 else
10905 {
10906 display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize);
10907 }
10908 }
10909 RenderNavCursor(bb, id);
10910
10911 // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10912 const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10913 if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10914 TabBarQueueFocus(tab_bar, tab);
10915
10916 if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10917 flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10918
10919 // Render tab label, process close button
10920 const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, docked_window ? docked_window->ID : id) : 0;
10921 bool just_closed;
10922 bool text_clipped;
10923 TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped);
10924 if (just_closed && p_open != NULL)
10925 {
10926 *p_open = false;
10927 TabBarCloseTab(tab_bar, tab);
10928 }
10929
10930 // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
10931 // That state is copied to window->DockTabItemStatusFlags by our caller.
10932 if (docked_window && (hovered || g.HoveredId == close_button_id))
10933 g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
10934
10935 // Tooltip
10936 // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10937 // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10938 // FIXME: This is a mess.
10939 // FIXME: We may want disabled tab to still display the tooltip?
10940 if (text_clipped && g.HoveredId == id && !held)
10941 if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10942 SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10943 }
10944
10945 // Restore main window position so user can draw there
10946 if (want_clip_rect)
10947 PopClipRect();
10948 window->DC.CursorPos = backup_main_cursor_pos;
10949
10950 IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10951 if (is_tab_button)
10952 return pressed;
10953 return tab_contents_visible;
10954}
10955
10956// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10957// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10958// Tabs closed by the close button will automatically be flagged to avoid this issue.
10959void ImGui::SetTabItemClosed(const char* label)
10960{
10961 ImGuiContext& g = *GImGui;
10962 bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10963 if (is_within_manual_tab_bar)
10964 {
10965 ImGuiTabBar* tab_bar = g.CurrentTabBar;
10966 ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10967 if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10968 tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10969 }
10970 else if (ImGuiWindow* window = FindWindowByName(label))
10971 {
10972 if (window->DockIsActive)
10973 if (ImGuiDockNode* node = window->DockNode)
10974 {
10975 ImGuiID tab_id = TabBarCalcTabID(node->TabBar, label, window);
10976 TabBarRemoveTab(node->TabBar, tab_id);
10977 window->DockTabWantClose = true;
10978 }
10979 }
10980}
10981
10982ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10983{
10984 ImGuiContext& g = *GImGui;
10985 ImVec2 label_size = CalcTextSize(label, NULL, true);
10986 ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10987 if (has_close_button_or_unsaved_marker)
10988 size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10989 else
10990 size.x += g.Style.FramePadding.x + 1.0f;
10991 return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10992}
10993
10994ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
10995{
10996 return TabItemCalcSize(window->Name, window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
10997}
10998
10999void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
11000{
11001 // 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.
11002 ImGuiContext& g = *GImGui;
11003 const float width = bb.GetWidth();
11004 IM_UNUSED(flags);
11005 IM_ASSERT(width > 0.0f);
11006 const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
11007 const float y1 = bb.Min.y + 1.0f; // Leave a bit of room in title bars.
11008 const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
11009 draw_list->AddRectFilled(bb.Min, ImVec2(bb.Max.x, y2), col, rounding, ImDrawFlags_RoundCornersTop);
11010 if (g.Style.TabBorderSize > 0.0f)
11011 {
11012 draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
11013 draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
11014 draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
11015 draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
11016 draw_list->PathStroke(GetColorU32(ImGuiCol_Border), g.Style.TabBorderSize);
11017 }
11018}
11019
11020// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
11021// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
11022void 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)
11023{
11024 ImGuiContext& g = *GImGui;
11025 const char* label_end = FindRenderedTextEnd(label);
11026 ImVec2 label_size = CalcTextSize(label, label_end, false);
11027
11028 if (out_just_closed)
11029 *out_just_closed = false;
11030 if (out_text_clipped)
11031 *out_text_clipped = false;
11032
11033 if (bb.GetWidth() <= 1.0f)
11034 return;
11035
11036 // In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
11037 // But right now if you want to alter text color of tabs this is what you need to do.
11038#if 0
11039 const float backup_alpha = g.Style.Alpha;
11040 if (!is_contents_visible)
11041 g.Style.Alpha *= 0.7f;
11042#endif
11043
11044 // Render text label (with clipping + alpha gradient) + unsaved marker
11045 ImRect text_ellipsis_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
11046
11047 // Return clipped state ignoring the close button
11048 if (out_text_clipped)
11049 {
11050 *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
11051 //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));
11052 }
11053
11054 const float button_sz = g.FontSize;
11055 const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
11056
11057 // Close Button & Unsaved Marker
11058 // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
11059 // 'hovered' will be true when hovering the Tab but NOT when hovering the close button
11060 // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
11061 // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
11062 bool close_button_pressed = false;
11063 bool close_button_visible = false;
11064 bool is_hovered = g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id; // Any interaction account for this too.
11065
11066 if (close_button_id != 0)
11067 {
11068 if (is_contents_visible)
11069 close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected));
11070 else
11071 close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected));
11072 }
11073
11074 // When tabs/document is unsaved, the unsaved marker takes priority over the close button.
11075 const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
11076 if (unsaved_marker_visible)
11077 {
11078 ImVec2 bullet_pos = button_pos + ImVec2(button_sz, button_sz) * 0.5f;
11079 RenderBullet(draw_list, bullet_pos, GetColorU32(ImGuiCol_UnsavedMarker));
11080 }
11081 else if (close_button_visible)
11082 {
11083 ImGuiLastItemData last_item_backup = g.LastItemData;
11084 if (CloseButton(close_button_id, button_pos))
11085 close_button_pressed = true;
11086 g.LastItemData = last_item_backup;
11087
11088 // Close with middle mouse button
11089 if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
11090 close_button_pressed = true;
11091 }
11092
11093 // This is all rather complicated
11094 // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
11095 // 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..
11096 float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
11097 if (close_button_visible || unsaved_marker_visible)
11098 {
11099 const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
11100 if (visible_without_hover)
11101 {
11102 text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
11103 ellipsis_max_x -= button_sz * 0.90f;
11104 }
11105 else
11106 {
11107 text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
11108 }
11109 }
11110 LogSetNextTextDecoration("/", "\\");
11111 RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, label_end, &label_size);
11112
11113#if 0
11114 if (!is_contents_visible)
11115 g.Style.Alpha = backup_alpha;
11116#endif
11117
11118 if (out_just_closed)
11119 *out_just_closed = close_button_pressed;
11120}
11121
11122
11123#endif // #ifndef IMGUI_DISABLE