Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/dep/imgui/src/imgui_widgets.cpp
4246 views
1
// dear imgui, v1.92.0
2
// (widgets code)
3
4
/*
5
6
Index 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
#endif
96
97
//-------------------------------------------------------------------------
98
// Data
99
//-------------------------------------------------------------------------
100
101
// Widgets
102
static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
103
static 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.
104
105
// Those MIN/MAX values are not define because we need to point to them
106
static const signed char IM_S8_MIN = -128;
107
static const signed char IM_S8_MAX = 127;
108
static const unsigned char IM_U8_MIN = 0;
109
static const unsigned char IM_U8_MAX = 0xFF;
110
static const signed short IM_S16_MIN = -32768;
111
static const signed short IM_S16_MAX = 32767;
112
static const unsigned short IM_U16_MIN = 0;
113
static const unsigned short IM_U16_MAX = 0xFFFF;
114
static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
115
static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
116
static const ImU32 IM_U32_MIN = 0;
117
static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
118
#ifdef LLONG_MIN
119
static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
120
static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
121
#else
122
static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
123
static const ImS64 IM_S64_MAX = 9223372036854775807LL;
124
#endif
125
static const ImU64 IM_U64_MIN = 0;
126
#ifdef ULLONG_MAX
127
static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
128
#else
129
static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
130
#endif
131
132
//-------------------------------------------------------------------------
133
// [SECTION] Forward Declarations
134
//-------------------------------------------------------------------------
135
136
// For InputTextEx()
137
static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
138
static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
139
static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
140
141
//-------------------------------------------------------------------------
142
// [SECTION] Widgets: Text, etc.
143
//-------------------------------------------------------------------------
144
// - TextEx() [Internal]
145
// - TextUnformatted()
146
// - Text()
147
// - TextV()
148
// - TextColored()
149
// - TextColoredV()
150
// - TextDisabled()
151
// - TextDisabledV()
152
// - TextWrapped()
153
// - TextWrappedV()
154
// - LabelText()
155
// - LabelTextV()
156
// - BulletText()
157
// - BulletTextV()
158
//-------------------------------------------------------------------------
159
160
void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
161
{
162
ImGuiWindow* window = GetCurrentWindow();
163
if (window->SkipItems)
164
return;
165
ImGuiContext& g = *GImGui;
166
167
// Accept null ranges
168
if (text == text_end)
169
text = text_end = "";
170
171
// Calculate length
172
const char* text_begin = text;
173
if (text_end == NULL)
174
text_end = text + ImStrlen(text); // FIXME-OPT
175
176
const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
177
const float wrap_pos_x = window->DC.TextWrapPos;
178
const bool wrap_enabled = (wrap_pos_x >= 0.0f);
179
if (text_end - text <= 2000 || wrap_enabled)
180
{
181
// Common case
182
const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
183
const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
184
185
ImRect bb(text_pos, text_pos + text_size);
186
ItemSize(text_size, 0.0f);
187
if (!ItemAdd(bb, 0))
188
return;
189
190
// Render (we don't hide text after ## in this end-user function)
191
RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
192
}
193
else
194
{
195
// Long text!
196
// Perform manual coarse clipping to optimize for long multi-line text
197
// - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
198
// - 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.
199
// - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
200
const char* line = text;
201
const float line_height = GetTextLineHeight();
202
ImVec2 text_size(0, 0);
203
204
// Lines to skip (can't skip when logging text)
205
ImVec2 pos = text_pos;
206
if (!g.LogEnabled)
207
{
208
int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
209
if (lines_skippable > 0)
210
{
211
int lines_skipped = 0;
212
while (line < text_end && lines_skipped < lines_skippable)
213
{
214
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
215
if (!line_end)
216
line_end = text_end;
217
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
218
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
219
line = line_end + 1;
220
lines_skipped++;
221
}
222
pos.y += lines_skipped * line_height;
223
}
224
}
225
226
// Lines to render
227
if (line < text_end)
228
{
229
ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
230
while (line < text_end)
231
{
232
if (IsClippedEx(line_rect, 0))
233
break;
234
235
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
236
if (!line_end)
237
line_end = text_end;
238
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
239
RenderText(pos, line, line_end, false);
240
line = line_end + 1;
241
line_rect.Min.y += line_height;
242
line_rect.Max.y += line_height;
243
pos.y += line_height;
244
}
245
246
// Count remaining lines
247
int lines_skipped = 0;
248
while (line < text_end)
249
{
250
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
251
if (!line_end)
252
line_end = text_end;
253
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
254
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
255
line = line_end + 1;
256
lines_skipped++;
257
}
258
pos.y += lines_skipped * line_height;
259
}
260
text_size.y = (pos - text_pos).y;
261
262
ImRect bb(text_pos, text_pos + text_size);
263
ItemSize(text_size, 0.0f);
264
ItemAdd(bb, 0);
265
}
266
}
267
268
void ImGui::TextUnformatted(const char* text, const char* text_end)
269
{
270
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
271
}
272
273
void ImGui::Text(const char* fmt, ...)
274
{
275
va_list args;
276
va_start(args, fmt);
277
TextV(fmt, args);
278
va_end(args);
279
}
280
281
void ImGui::TextV(const char* fmt, va_list args)
282
{
283
ImGuiWindow* window = GetCurrentWindow();
284
if (window->SkipItems)
285
return;
286
287
const char* text, *text_end;
288
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
289
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
290
}
291
292
void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
293
{
294
va_list args;
295
va_start(args, fmt);
296
TextColoredV(col, fmt, args);
297
va_end(args);
298
}
299
300
void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
301
{
302
PushStyleColor(ImGuiCol_Text, col);
303
TextV(fmt, args);
304
PopStyleColor();
305
}
306
307
void ImGui::TextDisabled(const char* fmt, ...)
308
{
309
va_list args;
310
va_start(args, fmt);
311
TextDisabledV(fmt, args);
312
va_end(args);
313
}
314
315
void ImGui::TextDisabledV(const char* fmt, va_list args)
316
{
317
ImGuiContext& g = *GImGui;
318
PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
319
TextV(fmt, args);
320
PopStyleColor();
321
}
322
323
void ImGui::TextWrapped(const char* fmt, ...)
324
{
325
va_list args;
326
va_start(args, fmt);
327
TextWrappedV(fmt, args);
328
va_end(args);
329
}
330
331
void ImGui::TextWrappedV(const char* fmt, va_list args)
332
{
333
ImGuiContext& g = *GImGui;
334
const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
335
if (need_backup)
336
PushTextWrapPos(0.0f);
337
TextV(fmt, args);
338
if (need_backup)
339
PopTextWrapPos();
340
}
341
342
void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
343
{
344
va_list args;
345
va_start(args, fmt);
346
TextAlignedV(align_x, size_x, fmt, args);
347
va_end(args);
348
}
349
350
// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
351
// size_x : 0.0f = shortcut for GetContentRegionAvail().x
352
// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
353
void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
354
{
355
ImGuiWindow* window = GetCurrentWindow();
356
if (window->SkipItems)
357
return;
358
359
const char* text, *text_end;
360
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
361
const ImVec2 text_size = CalcTextSize(text, text_end);
362
size_x = CalcItemSize(ImVec2(size_x, 0.0f), 0.0f, text_size.y).x;
363
364
ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
365
ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
366
ImVec2 size(ImMin(size_x, text_size.x), text_size.y);
367
window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, pos.x + text_size.x);
368
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, pos.x + text_size.x);
369
if (align_x > 0.0f && text_size.x < size_x)
370
pos.x += ImTrunc((size_x - text_size.x) * align_x);
371
RenderTextEllipsis(window->DrawList, pos, pos_max, pos_max.x, text, text_end, &text_size);
372
373
const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
374
ItemSize(size);
375
ItemAdd(ImRect(pos, pos + size), 0);
376
window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
377
378
if (size_x < text_size.x && IsItemHovered(ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
379
SetTooltip("%.*s", (int)(text_end - text), text);
380
}
381
382
void ImGui::LabelText(const char* label, const char* fmt, ...)
383
{
384
va_list args;
385
va_start(args, fmt);
386
LabelTextV(label, fmt, args);
387
va_end(args);
388
}
389
390
// Add a label+text combo aligned to other label+value widgets
391
void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
392
{
393
ImGuiWindow* window = GetCurrentWindow();
394
if (window->SkipItems)
395
return;
396
397
ImGuiContext& g = *GImGui;
398
const ImGuiStyle& style = g.Style;
399
const float w = CalcItemWidth();
400
401
const char* value_text_begin, *value_text_end;
402
ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
403
const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
404
const ImVec2 label_size = CalcTextSize(label, NULL, true);
405
406
const ImVec2 pos = window->DC.CursorPos;
407
const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
408
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));
409
ItemSize(total_bb, style.FramePadding.y);
410
if (!ItemAdd(total_bb, 0))
411
return;
412
413
// Render
414
RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
415
if (label_size.x > 0.0f)
416
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
417
}
418
419
void ImGui::BulletText(const char* fmt, ...)
420
{
421
va_list args;
422
va_start(args, fmt);
423
BulletTextV(fmt, args);
424
va_end(args);
425
}
426
427
// Text with a little bullet aligned to the typical tree node.
428
void ImGui::BulletTextV(const char* fmt, va_list args)
429
{
430
ImGuiWindow* window = GetCurrentWindow();
431
if (window->SkipItems)
432
return;
433
434
ImGuiContext& g = *GImGui;
435
const ImGuiStyle& style = g.Style;
436
437
const char* text_begin, *text_end;
438
ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
439
const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
440
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
441
ImVec2 pos = window->DC.CursorPos;
442
pos.y += window->DC.CurrLineTextBaseOffset;
443
ItemSize(total_size, 0.0f);
444
const ImRect bb(pos, pos + total_size);
445
if (!ItemAdd(bb, 0))
446
return;
447
448
// Render
449
ImU32 text_col = GetColorU32(ImGuiCol_Text);
450
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
451
RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
452
}
453
454
//-------------------------------------------------------------------------
455
// [SECTION] Widgets: Main
456
//-------------------------------------------------------------------------
457
// - ButtonBehavior() [Internal]
458
// - Button()
459
// - SmallButton()
460
// - InvisibleButton()
461
// - ArrowButton()
462
// - CloseButton() [Internal]
463
// - CollapseButton() [Internal]
464
// - GetWindowScrollbarID() [Internal]
465
// - GetWindowScrollbarRect() [Internal]
466
// - Scrollbar() [Internal]
467
// - ScrollbarEx() [Internal]
468
// - Image()
469
// - ImageButton()
470
// - Checkbox()
471
// - CheckboxFlagsT() [Internal]
472
// - CheckboxFlags()
473
// - RadioButton()
474
// - ProgressBar()
475
// - Bullet()
476
// - Hyperlink()
477
//-------------------------------------------------------------------------
478
479
// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
480
// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
481
// this code is a little complex.
482
// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
483
// See the series of events below and the corresponding state reported by dear imgui:
484
//------------------------------------------------------------------------------------------------------------------------------------------------
485
// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
486
// Frame N+0 (mouse is outside bb) - - - - - -
487
// Frame N+1 (mouse moves inside bb) - true - - - -
488
// Frame N+2 (mouse button is down) - true true true - true
489
// Frame N+3 (mouse button is down) - true true - - -
490
// Frame N+4 (mouse moves outside bb) - - true - - -
491
// Frame N+5 (mouse moves inside bb) - true true - - -
492
// Frame N+6 (mouse button is released) true true - - true -
493
// Frame N+7 (mouse button is released) - true - - - -
494
// Frame N+8 (mouse moves outside bb) - - - - - -
495
//------------------------------------------------------------------------------------------------------------------------------------------------
496
// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
497
// Frame N+2 (mouse button is down) true true true true - true
498
// Frame N+3 (mouse button is down) - true true - - -
499
// Frame N+6 (mouse button is released) - true - - true -
500
// Frame N+7 (mouse button is released) - true - - - -
501
//------------------------------------------------------------------------------------------------------------------------------------------------
502
// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
503
// Frame N+2 (mouse button is down) - true - - - true
504
// Frame N+3 (mouse button is down) - true - - - -
505
// Frame N+6 (mouse button is released) true true - - - -
506
// Frame N+7 (mouse button is released) - true - - - -
507
//------------------------------------------------------------------------------------------------------------------------------------------------
508
// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
509
// Frame N+0 (mouse button is down) - true - - - true
510
// Frame N+1 (mouse button is down) - true - - - -
511
// Frame N+2 (mouse button is released) - true - - - -
512
// Frame N+3 (mouse button is released) - true - - - -
513
// Frame N+4 (mouse button is down) true true true true - true
514
// Frame N+5 (mouse button is down) - true true - - -
515
// Frame N+6 (mouse button is released) - true - - true -
516
// Frame N+7 (mouse button is released) - true - - - -
517
//------------------------------------------------------------------------------------------------------------------------------------------------
518
// Note that some combinations are supported,
519
// - PressedOnDragDropHold can generally be associated with any flag.
520
// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
521
//------------------------------------------------------------------------------------------------------------------------------------------------
522
// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
523
// Repeat+ Repeat+ Repeat+ Repeat+
524
// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
525
//-------------------------------------------------------------------------------------------------------------------------------------------------
526
// Frame N+0 (mouse button is down) - true - true
527
// ... - - - -
528
// Frame N + RepeatDelay true true - true
529
// ... - - - -
530
// Frame N + RepeatDelay + RepeatRate*N true true - true
531
//-------------------------------------------------------------------------------------------------------------------------------------------------
532
533
// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
534
// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
535
// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
536
// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
537
// One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
538
// with same ID and different MouseButton (see #8030). You can fix it by:
539
// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
540
// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
541
bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
542
{
543
ImGuiContext& g = *GImGui;
544
ImGuiWindow* window = GetCurrentWindow();
545
546
// Default behavior inherited from item flags
547
// Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
548
ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
549
if (flags & ImGuiButtonFlags_AllowOverlap)
550
item_flags |= ImGuiItemFlags_AllowOverlap;
551
if (item_flags & ImGuiItemFlags_NoFocus)
552
flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
553
554
// Default only reacts to left mouse button
555
if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
556
flags |= ImGuiButtonFlags_MouseButtonLeft;
557
558
// Default behavior requires click + release inside bounding box
559
if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
560
flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
561
562
ImGuiWindow* backup_hovered_window = g.HoveredWindow;
563
const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window->RootWindow;
564
if (flatten_hovered_children)
565
g.HoveredWindow = window;
566
567
#ifdef IMGUI_ENABLE_TEST_ENGINE
568
// Alternate registration spot, for when caller didn't use ItemAdd()
569
if (g.LastItemData.ID != id)
570
IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
571
#endif
572
573
bool pressed = false;
574
bool hovered = ItemHoverable(bb, id, item_flags);
575
576
// Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
577
if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
578
if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
579
{
580
hovered = true;
581
SetHoveredID(id);
582
if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
583
{
584
pressed = true;
585
g.DragDropHoldJustPressedId = id;
586
FocusWindow(window);
587
}
588
}
589
590
if (flatten_hovered_children)
591
g.HoveredWindow = backup_hovered_window;
592
593
// Mouse handling
594
const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
595
if (hovered)
596
{
597
IM_ASSERT(id != 0); // Lazily check inside rare path.
598
599
// Poll mouse buttons
600
// - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
601
// - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
602
int mouse_button_clicked = -1;
603
int mouse_button_released = -1;
604
for (int button = 0; button < 3; button++)
605
if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
606
{
607
if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
608
if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
609
}
610
611
// Process initial action
612
const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
613
if (mods_ok)
614
{
615
if (mouse_button_clicked != -1 && g.ActiveId != id)
616
{
617
if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
618
SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
619
if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
620
{
621
SetActiveID(id, window);
622
g.ActiveIdMouseButton = mouse_button_clicked;
623
if (!(flags & ImGuiButtonFlags_NoNavFocus))
624
{
625
SetFocusID(id, window);
626
FocusWindow(window);
627
}
628
else if (!(flags & ImGuiButtonFlags_NoFocus))
629
{
630
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
631
}
632
}
633
if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
634
{
635
pressed = true;
636
if (flags & ImGuiButtonFlags_NoHoldingActiveId)
637
ClearActiveID();
638
else
639
SetActiveID(id, window); // Hold on ID
640
g.ActiveIdMouseButton = mouse_button_clicked;
641
if (!(flags & ImGuiButtonFlags_NoNavFocus))
642
{
643
SetFocusID(id, window);
644
FocusWindow(window);
645
}
646
else if (!(flags & ImGuiButtonFlags_NoFocus))
647
{
648
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
649
}
650
}
651
}
652
if (flags & ImGuiButtonFlags_PressedOnRelease)
653
{
654
if (mouse_button_released != -1)
655
{
656
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
657
if (!has_repeated_at_least_once)
658
pressed = true;
659
if (!(flags & ImGuiButtonFlags_NoNavFocus))
660
SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
661
ClearActiveID();
662
}
663
}
664
665
// 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
666
// Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
667
if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
668
if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
669
pressed = true;
670
}
671
672
if (pressed && g.IO.ConfigNavCursorVisibleAuto)
673
g.NavCursorVisible = false;
674
}
675
676
// Keyboard/Gamepad navigation handling
677
// We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
678
if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
679
if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
680
hovered = true;
681
if (g.NavActivateDownId == id)
682
{
683
bool nav_activated_by_code = (g.NavActivateId == id);
684
bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
685
if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
686
{
687
// Avoid pressing multiple keys from triggering excessive amount of repeat events
688
const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
689
const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
690
const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
691
const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
692
nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
693
}
694
if (nav_activated_by_code || nav_activated_by_inputs)
695
{
696
// Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
697
pressed = true;
698
SetActiveID(id, window);
699
g.ActiveIdSource = g.NavInputSource;
700
if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
701
SetFocusID(id, window);
702
if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
703
g.ActiveIdFromShortcut = true;
704
}
705
}
706
707
// Process while held
708
bool held = false;
709
if (g.ActiveId == id)
710
{
711
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
712
{
713
if (g.ActiveIdIsJustActivated)
714
g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
715
716
const int mouse_button = g.ActiveIdMouseButton;
717
if (mouse_button == -1)
718
{
719
// Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
720
ClearActiveID();
721
}
722
else if (IsMouseDown(mouse_button, test_owner_id))
723
{
724
held = true;
725
}
726
else
727
{
728
bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
729
bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
730
if ((release_in || release_anywhere) && !g.DragDropActive)
731
{
732
// Report as pressed when releasing the mouse (this is the most common path)
733
bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
734
bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
735
bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
736
if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
737
pressed = true;
738
}
739
ClearActiveID();
740
}
741
if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
742
g.NavCursorVisible = false;
743
}
744
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
745
{
746
// When activated using Nav, we hold on the ActiveID until activation button is released
747
if (g.NavActivateDownId == id)
748
held = true; // hovered == true not true as we are already likely hovered on direct activation.
749
else
750
ClearActiveID();
751
}
752
if (pressed)
753
g.ActiveIdHasBeenPressedBefore = true;
754
}
755
756
// Activation highlight (this may be a remote activation)
757
if (g.NavHighlightActivatedId == id)
758
hovered = true;
759
760
if (out_hovered) *out_hovered = hovered;
761
if (out_held) *out_held = held;
762
763
return pressed;
764
}
765
766
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
767
{
768
ImGuiWindow* window = GetCurrentWindow();
769
if (window->SkipItems)
770
return false;
771
772
ImGuiContext& g = *GImGui;
773
const ImGuiStyle& style = g.Style;
774
const ImGuiID id = window->GetID(label);
775
const ImVec2 label_size = CalcTextSize(label, NULL, true);
776
777
ImVec2 pos = window->DC.CursorPos;
778
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)
779
pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
780
ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
781
782
const ImRect bb(pos, pos + size);
783
ItemSize(size, style.FramePadding.y);
784
if (!ItemAdd(bb, id))
785
return false;
786
787
bool hovered, held;
788
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
789
790
// Render
791
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
792
RenderNavCursor(bb, id);
793
RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
794
795
if (g.LogEnabled)
796
LogSetNextTextDecoration("[", "]");
797
RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb);
798
799
// Automatically close popups
800
//if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
801
// CloseCurrentPopup();
802
803
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
804
return pressed;
805
}
806
807
bool ImGui::Button(const char* label, const ImVec2& size_arg)
808
{
809
return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
810
}
811
812
// Small buttons fits within text without additional vertical spacing.
813
bool ImGui::SmallButton(const char* label)
814
{
815
ImGuiContext& g = *GImGui;
816
float backup_padding_y = g.Style.FramePadding.y;
817
g.Style.FramePadding.y = 0.0f;
818
bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
819
g.Style.FramePadding.y = backup_padding_y;
820
return pressed;
821
}
822
823
// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
824
// 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)
825
bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
826
{
827
ImGuiContext& g = *GImGui;
828
ImGuiWindow* window = GetCurrentWindow();
829
if (window->SkipItems)
830
return false;
831
832
// Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
833
IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
834
835
const ImGuiID id = window->GetID(str_id);
836
ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f);
837
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
838
ItemSize(size);
839
if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
840
return false;
841
842
bool hovered, held;
843
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
844
RenderNavCursor(bb, id);
845
846
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
847
return pressed;
848
}
849
850
bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
851
{
852
ImGuiContext& g = *GImGui;
853
ImGuiWindow* window = GetCurrentWindow();
854
if (window->SkipItems)
855
return false;
856
857
const ImGuiID id = window->GetID(str_id);
858
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
859
const float default_size = GetFrameHeight();
860
ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
861
if (!ItemAdd(bb, id))
862
return false;
863
864
bool hovered, held;
865
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
866
867
// Render
868
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
869
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
870
RenderNavCursor(bb, id);
871
RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
872
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);
873
874
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
875
return pressed;
876
}
877
878
bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
879
{
880
float sz = GetFrameHeight();
881
return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
882
}
883
884
// Button to close a window
885
bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
886
{
887
ImGuiContext& g = *GImGui;
888
ImGuiWindow* window = g.CurrentWindow;
889
890
// 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)
891
// 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?
892
const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
893
ImRect bb_interact = bb;
894
const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
895
if (area_to_visible_ratio < 1.5f)
896
bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
897
898
// Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
899
// (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).
900
bool is_clipped = !ItemAdd(bb_interact, id);
901
902
bool hovered, held;
903
bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
904
if (is_clipped)
905
return pressed;
906
907
// Render
908
ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
909
if (hovered)
910
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
911
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
912
const ImU32 cross_col = GetColorU32(ImGuiCol_Text);
913
const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
914
const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
915
const float cross_thickness = 1.0f; // FIXME-DPI
916
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness);
917
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness);
918
919
return pressed;
920
}
921
922
bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
923
{
924
ImGuiContext& g = *GImGui;
925
ImGuiWindow* window = g.CurrentWindow;
926
927
ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
928
bool is_clipped = !ItemAdd(bb, id);
929
bool hovered, held;
930
bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
931
if (is_clipped)
932
return pressed;
933
934
// Render
935
ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
936
ImU32 text_col = GetColorU32(ImGuiCol_Text);
937
if (hovered || held)
938
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
939
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
940
RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
941
942
// Switch to moving the window after mouse is moved beyond the initial drag threshold
943
if (IsItemActive() && IsMouseDragging(0))
944
StartMouseMovingWindow(window);
945
946
return pressed;
947
}
948
949
ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
950
{
951
return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
952
}
953
954
// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
955
ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
956
{
957
ImGuiContext& g = *GImGui;
958
const ImRect outer_rect = window->Rect();
959
const ImRect inner_rect = window->InnerRect;
960
const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
961
IM_ASSERT(scrollbar_size >= 0.0f);
962
const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
963
const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f;
964
if (axis == ImGuiAxis_X)
965
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);
966
else
967
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);
968
}
969
970
void ImGui::Scrollbar(ImGuiAxis axis)
971
{
972
ImGuiContext& g = *GImGui;
973
ImGuiWindow* window = g.CurrentWindow;
974
const ImGuiID id = GetWindowScrollbarID(window, axis);
975
976
// Calculate scrollbar bounding box
977
ImRect bb = GetWindowScrollbarRect(window, axis);
978
ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
979
if (axis == ImGuiAxis_X)
980
{
981
rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
982
if (!window->ScrollbarY)
983
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
984
}
985
else
986
{
987
if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
988
rounding_corners |= ImDrawFlags_RoundCornersTopRight;
989
if (!window->ScrollbarX)
990
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
991
}
992
float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
993
float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
994
ImS64 scroll = (ImS64)window->ScrollExpected[axis];
995
ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
996
window->ScrollExpected[axis] = (float)scroll;
997
}
998
999
// Vertical/Horizontal scrollbar
1000
// The entire piece of code below is rather confusing because:
1001
// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
1002
// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
1003
// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
1004
// Still, the code should probably be made simpler..
1005
bool 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)
1006
{
1007
ImGuiContext& g = *GImGui;
1008
ImGuiWindow* window = g.CurrentWindow;
1009
if (window->SkipItems)
1010
return false;
1011
1012
const float bb_frame_width = bb_frame.GetWidth();
1013
const float bb_frame_height = bb_frame.GetHeight();
1014
if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
1015
return false;
1016
1017
// 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)
1018
float alpha = 1.0f;
1019
if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
1020
alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f));
1021
if (alpha <= 0.0f)
1022
return false;
1023
1024
const ImGuiStyle& style = g.Style;
1025
const bool allow_interaction = (alpha >= 1.0f);
1026
1027
ImRect bb = bb_frame;
1028
bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f)));
1029
1030
// V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
1031
const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
1032
1033
// Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
1034
// But we maintain a minimum size in pixel to allow for the user to still aim inside.
1035
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.
1036
const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
1037
const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize);
1038
const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v);
1039
const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1040
1041
// Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1042
bool held = false;
1043
bool hovered = false;
1044
ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
1045
ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
1046
1047
const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1048
float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1049
float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1050
if (held && allow_interaction && grab_h_norm < 1.0f)
1051
{
1052
const float scrollbar_pos_v = bb.Min[axis];
1053
const float mouse_pos_v = g.IO.MousePos[axis];
1054
1055
// Click position in scrollbar normalized space (0.0f->1.0f)
1056
const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1057
1058
const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1059
if (g.ActiveIdIsJustActivated)
1060
{
1061
// On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1062
const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1063
g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1064
g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1065
}
1066
1067
// Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1068
// 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
1069
if (g.ScrollbarSeekMode == 0)
1070
{
1071
// Absolute seeking
1072
const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1073
*p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1074
}
1075
else
1076
{
1077
// Page by page
1078
if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1079
{
1080
float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1081
*p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1082
}
1083
}
1084
1085
// Update values for rendering
1086
scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1087
grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1088
g.ScrollbarHeld |= 2;
1089
1090
// Update distance to grab now that we have seek'ed and saturated
1091
//if (seek_absolute)
1092
// g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1093
}
1094
1095
// Render
1096
const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1097
const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1098
window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1099
ImRect grab_rect;
1100
if (axis == ImGuiAxis_X)
1101
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);
1102
else
1103
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);
1104
window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1105
1106
return held;
1107
}
1108
1109
// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1110
// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1111
void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1112
{
1113
ImGuiContext& g = *GImGui;
1114
ImGuiWindow* window = GetCurrentWindow();
1115
if (window->SkipItems)
1116
return;
1117
1118
const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1119
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1120
ItemSize(bb);
1121
if (!ItemAdd(bb, 0))
1122
return;
1123
1124
// Render
1125
if (g.Style.ImageBorderSize > 0.0f)
1126
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, ImDrawFlags_None, g.Style.ImageBorderSize);
1127
if (bg_col.w > 0.0f)
1128
window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1129
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1130
}
1131
1132
void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1133
{
1134
ImageWithBg(tex_ref, image_size, uv0, uv1);
1135
}
1136
1137
// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1138
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1139
void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1140
{
1141
ImGuiContext& g = *GImGui;
1142
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
1143
PushStyleColor(ImGuiCol_Border, border_col);
1144
ImageWithBg(tex_ref, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
1145
PopStyleColor();
1146
PopStyleVar();
1147
}
1148
#endif
1149
1150
bool 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)
1151
{
1152
ImGuiContext& g = *GImGui;
1153
ImGuiWindow* window = GetCurrentWindow();
1154
if (window->SkipItems)
1155
return false;
1156
1157
const ImVec2 padding = g.Style.FramePadding;
1158
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1159
ItemSize(bb);
1160
if (!ItemAdd(bb, id))
1161
return false;
1162
1163
bool hovered, held;
1164
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1165
1166
// Render
1167
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1168
RenderNavCursor(bb, id);
1169
RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding));
1170
if (bg_col.w > 0.0f)
1171
window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1172
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1173
1174
return pressed;
1175
}
1176
1177
// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1178
// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1179
bool 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)
1180
{
1181
ImGuiContext& g = *GImGui;
1182
ImGuiWindow* window = g.CurrentWindow;
1183
if (window->SkipItems)
1184
return false;
1185
1186
return ImageButtonEx(window->GetID(str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
1187
}
1188
1189
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1190
// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1191
// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1192
// - new ImageButton() requires an explicit 'const char* str_id'
1193
// - old ImageButton() had frame_padding' override argument.
1194
// - new ImageButton() always use style.FramePadding.
1195
/*
1196
bool 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)
1197
{
1198
// Default to using texture ID as ID. User can still push string/integer prefixes.
1199
PushID((ImTextureID)(intptr_t)user_texture_id);
1200
if (frame_padding >= 0)
1201
PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1202
bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1203
if (frame_padding >= 0)
1204
PopStyleVar();
1205
PopID();
1206
return ret;
1207
}
1208
*/
1209
#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1210
1211
bool ImGui::Checkbox(const char* label, bool* v)
1212
{
1213
ImGuiWindow* window = GetCurrentWindow();
1214
if (window->SkipItems)
1215
return false;
1216
1217
ImGuiContext& g = *GImGui;
1218
const ImGuiStyle& style = g.Style;
1219
const ImGuiID id = window->GetID(label);
1220
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1221
1222
const float square_sz = GetFrameHeight();
1223
const ImVec2 pos = window->DC.CursorPos;
1224
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));
1225
ItemSize(total_bb, style.FramePadding.y);
1226
const bool is_visible = ItemAdd(total_bb, id);
1227
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1228
if (!is_visible)
1229
if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1230
{
1231
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1232
return false;
1233
}
1234
1235
// Range-Selection/Multi-selection support (header)
1236
bool checked = *v;
1237
if (is_multi_select)
1238
MultiSelectItemHeader(id, &checked, NULL);
1239
1240
bool hovered, held;
1241
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1242
1243
// Range-Selection/Multi-selection support (footer)
1244
if (is_multi_select)
1245
MultiSelectItemFooter(id, &checked, &pressed);
1246
else if (pressed)
1247
checked = !checked;
1248
1249
if (*v != checked)
1250
{
1251
*v = checked;
1252
pressed = true; // return value
1253
MarkItemEdited(id);
1254
}
1255
1256
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1257
const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1258
if (is_visible)
1259
{
1260
RenderNavCursor(total_bb, id);
1261
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
1262
ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1263
if (mixed_value)
1264
{
1265
// Undocumented tristate/mixed/indeterminate checkbox (#2644)
1266
// This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1267
ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1268
window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1269
}
1270
else if (*v)
1271
{
1272
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1273
RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1274
}
1275
}
1276
const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1277
if (g.LogEnabled)
1278
LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1279
if (is_visible && label_size.x > 0.0f)
1280
RenderText(label_pos, label);
1281
1282
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1283
return pressed;
1284
}
1285
1286
template<typename T>
1287
bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1288
{
1289
bool all_on = (*flags & flags_value) == flags_value;
1290
bool any_on = (*flags & flags_value) != 0;
1291
bool pressed;
1292
if (!all_on && any_on)
1293
{
1294
ImGuiContext& g = *GImGui;
1295
g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1296
pressed = Checkbox(label, &all_on);
1297
}
1298
else
1299
{
1300
pressed = Checkbox(label, &all_on);
1301
1302
}
1303
if (pressed)
1304
{
1305
if (all_on)
1306
*flags |= flags_value;
1307
else
1308
*flags &= ~flags_value;
1309
}
1310
return pressed;
1311
}
1312
1313
bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1314
{
1315
return CheckboxFlagsT(label, flags, flags_value);
1316
}
1317
1318
bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1319
{
1320
return CheckboxFlagsT(label, flags, flags_value);
1321
}
1322
1323
bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1324
{
1325
return CheckboxFlagsT(label, flags, flags_value);
1326
}
1327
1328
bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1329
{
1330
return CheckboxFlagsT(label, flags, flags_value);
1331
}
1332
1333
bool ImGui::RadioButton(const char* label, bool active)
1334
{
1335
ImGuiWindow* window = GetCurrentWindow();
1336
if (window->SkipItems)
1337
return false;
1338
1339
ImGuiContext& g = *GImGui;
1340
const ImGuiStyle& style = g.Style;
1341
const ImGuiID id = window->GetID(label);
1342
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1343
1344
const float square_sz = GetFrameHeight();
1345
const ImVec2 pos = window->DC.CursorPos;
1346
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1347
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));
1348
ItemSize(total_bb, style.FramePadding.y);
1349
if (!ItemAdd(total_bb, id))
1350
return false;
1351
1352
ImVec2 center = check_bb.GetCenter();
1353
center.x = IM_ROUND(center.x);
1354
center.y = IM_ROUND(center.y);
1355
const float radius = (square_sz - 1.0f) * 0.5f;
1356
1357
bool hovered, held;
1358
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1359
if (pressed)
1360
MarkItemEdited(id);
1361
1362
RenderNavCursor(total_bb, id);
1363
const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1364
window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1365
if (active)
1366
{
1367
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1368
window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1369
}
1370
1371
if (style.FrameBorderSize > 0.0f)
1372
{
1373
window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1374
window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1375
}
1376
1377
ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1378
if (g.LogEnabled)
1379
LogRenderedText(&label_pos, active ? "(x)" : "( )");
1380
if (label_size.x > 0.0f)
1381
RenderText(label_pos, label);
1382
1383
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1384
return pressed;
1385
}
1386
1387
// 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..
1388
bool ImGui::RadioButton(const char* label, int* v, int v_button)
1389
{
1390
const bool pressed = RadioButton(label, *v == v_button);
1391
if (pressed)
1392
*v = v_button;
1393
return pressed;
1394
}
1395
1396
// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1397
void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1398
{
1399
ImGuiWindow* window = GetCurrentWindow();
1400
if (window->SkipItems)
1401
return;
1402
1403
ImGuiContext& g = *GImGui;
1404
const ImGuiStyle& style = g.Style;
1405
1406
ImVec2 pos = window->DC.CursorPos;
1407
ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1408
ImRect bb(pos, pos + size);
1409
ItemSize(size, style.FramePadding.y);
1410
if (!ItemAdd(bb, 0))
1411
return;
1412
1413
// Fraction < 0.0f will display an indeterminate progress bar animation
1414
// The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1415
const bool is_indeterminate = (fraction < 0.0f);
1416
if (!is_indeterminate)
1417
fraction = ImSaturate(fraction);
1418
1419
// Out of courtesy we accept a NaN fraction without crashing
1420
float fill_n0 = 0.0f;
1421
float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1422
1423
if (is_indeterminate)
1424
{
1425
const float fill_width_n = 0.2f;
1426
fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1427
fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1428
fill_n0 = ImSaturate(fill_n0);
1429
}
1430
1431
// Render
1432
RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1433
bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1434
RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding);
1435
1436
// Default displaying the fraction as percentage string, but user can override it
1437
// Don't display text for indeterminate bars by default
1438
char overlay_buf[32];
1439
if (!is_indeterminate || overlay != NULL)
1440
{
1441
if (!overlay)
1442
{
1443
ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1444
overlay = overlay_buf;
1445
}
1446
1447
ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1448
if (overlay_size.x > 0.0f)
1449
{
1450
float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x;
1451
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);
1452
}
1453
}
1454
}
1455
1456
void ImGui::Bullet()
1457
{
1458
ImGuiWindow* window = GetCurrentWindow();
1459
if (window->SkipItems)
1460
return;
1461
1462
ImGuiContext& g = *GImGui;
1463
const ImGuiStyle& style = g.Style;
1464
const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1465
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1466
ItemSize(bb);
1467
if (!ItemAdd(bb, 0))
1468
{
1469
SameLine(0, style.FramePadding.x * 2);
1470
return;
1471
}
1472
1473
// Render and stay on same line
1474
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1475
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1476
SameLine(0, style.FramePadding.x * 2.0f);
1477
}
1478
1479
// This is provided as a convenience for being an often requested feature.
1480
// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1481
// Because of this we currently don't provide many styling options for this widget
1482
// (e.g. hovered/active colors are automatically inferred from a single color).
1483
bool ImGui::TextLink(const char* label)
1484
{
1485
ImGuiWindow* window = GetCurrentWindow();
1486
if (window->SkipItems)
1487
return false;
1488
1489
ImGuiContext& g = *GImGui;
1490
const ImGuiID id = window->GetID(label);
1491
const char* label_end = FindRenderedTextEnd(label);
1492
1493
ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1494
ImVec2 size = CalcTextSize(label, label_end, true);
1495
ImRect bb(pos, pos + size);
1496
ItemSize(size, 0.0f);
1497
if (!ItemAdd(bb, id))
1498
return false;
1499
1500
bool hovered, held;
1501
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1502
RenderNavCursor(bb, id);
1503
1504
if (hovered)
1505
SetMouseCursor(ImGuiMouseCursor_Hand);
1506
1507
ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1508
ImVec4 line_colf = text_colf;
1509
{
1510
// FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1511
// as we are currently experimenting/planning a different styling system.
1512
float h, s, v;
1513
ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1514
if (held || hovered)
1515
{
1516
v = ImSaturate(v + (held ? 0.4f : 0.3f));
1517
h = ImFmod(h + 0.02f, 1.0f);
1518
}
1519
ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1520
v = ImSaturate(v - 0.20f);
1521
ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1522
}
1523
1524
float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
1525
window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode // FIXME-DPI
1526
1527
PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1528
RenderText(bb.Min, label, label_end);
1529
PopStyleColor();
1530
1531
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1532
return pressed;
1533
}
1534
1535
bool ImGui::TextLinkOpenURL(const char* label, const char* url)
1536
{
1537
ImGuiContext& g = *GImGui;
1538
if (url == NULL)
1539
url = label;
1540
bool pressed = TextLink(label);
1541
if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
1542
g.PlatformIO.Platform_OpenInShellFn(&g, url);
1543
SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1544
if (BeginPopupContextItem())
1545
{
1546
if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1547
SetClipboardText(url);
1548
EndPopup();
1549
}
1550
return pressed;
1551
}
1552
1553
//-------------------------------------------------------------------------
1554
// [SECTION] Widgets: Low-level Layout helpers
1555
//-------------------------------------------------------------------------
1556
// - Spacing()
1557
// - Dummy()
1558
// - NewLine()
1559
// - AlignTextToFramePadding()
1560
// - SeparatorEx() [Internal]
1561
// - Separator()
1562
// - SplitterBehavior() [Internal]
1563
// - ShrinkWidths() [Internal]
1564
//-------------------------------------------------------------------------
1565
1566
void ImGui::Spacing()
1567
{
1568
ImGuiWindow* window = GetCurrentWindow();
1569
if (window->SkipItems)
1570
return;
1571
ItemSize(ImVec2(0, 0));
1572
}
1573
1574
void ImGui::Dummy(const ImVec2& size)
1575
{
1576
ImGuiWindow* window = GetCurrentWindow();
1577
if (window->SkipItems)
1578
return;
1579
1580
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1581
ItemSize(size);
1582
ItemAdd(bb, 0);
1583
}
1584
1585
void ImGui::NewLine()
1586
{
1587
ImGuiWindow* window = GetCurrentWindow();
1588
if (window->SkipItems)
1589
return;
1590
1591
ImGuiContext& g = *GImGui;
1592
const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1593
window->DC.LayoutType = ImGuiLayoutType_Vertical;
1594
window->DC.IsSameLine = false;
1595
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.
1596
ItemSize(ImVec2(0, 0));
1597
else
1598
ItemSize(ImVec2(0.0f, g.FontSize));
1599
window->DC.LayoutType = backup_layout_type;
1600
}
1601
1602
void ImGui::AlignTextToFramePadding()
1603
{
1604
ImGuiWindow* window = GetCurrentWindow();
1605
if (window->SkipItems)
1606
return;
1607
1608
ImGuiContext& g = *GImGui;
1609
window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1610
window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1611
}
1612
1613
// Horizontal/vertical separating line
1614
// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1615
// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1616
void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1617
{
1618
ImGuiWindow* window = GetCurrentWindow();
1619
if (window->SkipItems)
1620
return;
1621
1622
ImGuiContext& g = *GImGui;
1623
IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1624
IM_ASSERT(thickness > 0.0f);
1625
1626
if (flags & ImGuiSeparatorFlags_Vertical)
1627
{
1628
// Vertical separator, for menu bars (use current line height).
1629
float y1 = window->DC.CursorPos.y;
1630
float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1631
const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1632
ItemSize(ImVec2(thickness, 0.0f));
1633
if (!ItemAdd(bb, 0))
1634
return;
1635
1636
// Draw
1637
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1638
if (g.LogEnabled)
1639
LogText(" |");
1640
}
1641
else if (flags & ImGuiSeparatorFlags_Horizontal)
1642
{
1643
// Horizontal Separator
1644
float x1 = window->DC.CursorPos.x;
1645
float x2 = window->WorkRect.Max.x;
1646
1647
// Preserve legacy behavior inside Columns()
1648
// Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1649
// We currently don't need to provide the same feature for tables because tables naturally have border features.
1650
ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1651
if (columns)
1652
{
1653
x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1654
x2 = window->Pos.x + window->Size.x;
1655
PushColumnsBackground();
1656
}
1657
1658
// We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1659
// FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1660
const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change.
1661
const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1662
ItemSize(ImVec2(0.0f, thickness_for_layout));
1663
1664
if (ItemAdd(bb, 0))
1665
{
1666
// Draw
1667
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1668
if (g.LogEnabled)
1669
LogRenderedText(&bb.Min, "--------------------------------\n");
1670
1671
}
1672
if (columns)
1673
{
1674
PopColumnsBackground();
1675
columns->LineMinY = window->DC.CursorPos.y;
1676
}
1677
}
1678
}
1679
1680
void ImGui::Separator()
1681
{
1682
ImGuiContext& g = *GImGui;
1683
ImGuiWindow* window = g.CurrentWindow;
1684
if (window->SkipItems)
1685
return;
1686
1687
// Those flags should eventually be configurable by the user
1688
// FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1689
ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1690
1691
// Only applies to legacy Columns() api as they relied on Separator() a lot.
1692
if (window->DC.CurrentColumns)
1693
flags |= ImGuiSeparatorFlags_SpanAllColumns;
1694
1695
SeparatorEx(flags, 1.0f);
1696
}
1697
1698
void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1699
{
1700
ImGuiContext& g = *GImGui;
1701
ImGuiWindow* window = g.CurrentWindow;
1702
ImGuiStyle& style = g.Style;
1703
1704
const ImVec2 label_size = CalcTextSize(label, label_end, false);
1705
const ImVec2 pos = window->DC.CursorPos;
1706
const ImVec2 padding = style.SeparatorTextPadding;
1707
1708
const float separator_thickness = style.SeparatorTextBorderSize;
1709
const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1710
const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1711
const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImFloor((style.SeparatorTextSize - label_size.y) * 0.5f));
1712
ItemSize(min_size, text_baseline_y);
1713
if (!ItemAdd(bb, id))
1714
return;
1715
1716
const float sep1_x1 = pos.x;
1717
const float sep2_x2 = bb.Max.x;
1718
const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1719
1720
const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1721
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
1722
1723
// This allows using SameLine() to position something in the 'extra_w'
1724
window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1725
1726
const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1727
if (label_size.x > 0.0f)
1728
{
1729
const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1730
const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1731
if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1732
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness);
1733
if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1734
window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1735
if (g.LogEnabled)
1736
LogSetNextTextDecoration("---", NULL);
1737
RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
1738
}
1739
else
1740
{
1741
if (g.LogEnabled)
1742
LogText("---");
1743
if (separator_thickness > 0.0f)
1744
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1745
}
1746
}
1747
1748
void ImGui::SeparatorText(const char* label)
1749
{
1750
ImGuiWindow* window = GetCurrentWindow();
1751
if (window->SkipItems)
1752
return;
1753
1754
// The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1755
// - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1756
// - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1757
// - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1758
// Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1759
// and then we can turn this into a format function.
1760
SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1761
}
1762
1763
// 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.
1764
bool 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)
1765
{
1766
ImGuiContext& g = *GImGui;
1767
ImGuiWindow* window = g.CurrentWindow;
1768
1769
if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1770
return false;
1771
1772
// FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1773
// to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1774
// Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1775
ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1776
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1777
button_flags |= ImGuiButtonFlags_AllowOverlap;
1778
#endif
1779
1780
bool hovered, held;
1781
ImRect bb_interact = bb;
1782
bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1783
ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1784
if (hovered)
1785
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1786
1787
if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1788
SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1789
1790
ImRect bb_render = bb;
1791
if (held)
1792
{
1793
float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1794
1795
// Minimum pane size
1796
float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1797
float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1798
if (mouse_delta < -size_1_maximum_delta)
1799
mouse_delta = -size_1_maximum_delta;
1800
if (mouse_delta > size_2_maximum_delta)
1801
mouse_delta = size_2_maximum_delta;
1802
1803
// Apply resize
1804
if (mouse_delta != 0.0f)
1805
{
1806
*size1 = ImMax(*size1 + mouse_delta, min_size1);
1807
*size2 = ImMax(*size2 - mouse_delta, min_size2);
1808
bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1809
MarkItemEdited(id);
1810
}
1811
}
1812
1813
// Render at new position
1814
if (bg_col & IM_COL32_A_MASK)
1815
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1816
const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1817
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1818
1819
return held;
1820
}
1821
1822
static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1823
{
1824
const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1825
const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1826
if (int d = (int)(b->Width - a->Width))
1827
return d;
1828
return (b->Index - a->Index);
1829
}
1830
1831
// Shrink excess width from a set of item, by removing width from the larger items first.
1832
// Set items Width to -1.0f to disable shrinking this item.
1833
void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1834
{
1835
if (count == 1)
1836
{
1837
if (items[0].Width >= 0.0f)
1838
items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
1839
return;
1840
}
1841
ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer);
1842
int count_same_width = 1;
1843
while (width_excess > 0.0f && count_same_width < count)
1844
{
1845
while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1846
count_same_width++;
1847
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);
1848
if (max_width_to_remove_per_item <= 0.0f)
1849
break;
1850
float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1851
for (int item_n = 0; item_n < count_same_width; item_n++)
1852
items[item_n].Width -= width_to_remove_per_item;
1853
width_excess -= width_to_remove_per_item * count_same_width;
1854
}
1855
1856
// Round width and redistribute remainder
1857
// 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.
1858
width_excess = 0.0f;
1859
for (int n = 0; n < count; n++)
1860
{
1861
float width_rounded = ImTrunc(items[n].Width);
1862
width_excess += items[n].Width - width_rounded;
1863
items[n].Width = width_rounded;
1864
}
1865
while (width_excess > 0.0f)
1866
for (int n = 0; n < count && width_excess > 0.0f; n++)
1867
{
1868
float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1869
items[n].Width += width_to_add;
1870
width_excess -= width_to_add;
1871
}
1872
}
1873
1874
//-------------------------------------------------------------------------
1875
// [SECTION] Widgets: ComboBox
1876
//-------------------------------------------------------------------------
1877
// - CalcMaxPopupHeightFromItemCount() [Internal]
1878
// - BeginCombo()
1879
// - BeginComboPopup() [Internal]
1880
// - EndCombo()
1881
// - BeginComboPreview() [Internal]
1882
// - EndComboPreview() [Internal]
1883
// - Combo()
1884
//-------------------------------------------------------------------------
1885
1886
static float CalcMaxPopupHeightFromItemCount(int items_count)
1887
{
1888
ImGuiContext& g = *GImGui;
1889
if (items_count <= 0)
1890
return FLT_MAX;
1891
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1892
}
1893
1894
bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1895
{
1896
ImGuiContext& g = *GImGui;
1897
ImGuiWindow* window = GetCurrentWindow();
1898
1899
ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1900
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1901
if (window->SkipItems)
1902
return false;
1903
1904
const ImGuiStyle& style = g.Style;
1905
const ImGuiID id = window->GetID(label);
1906
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1907
if (flags & ImGuiComboFlags_WidthFitPreview)
1908
IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1909
1910
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1911
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1912
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f;
1913
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1914
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1915
const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1916
ItemSize(total_bb, style.FramePadding.y);
1917
if (!ItemAdd(total_bb, id, &bb))
1918
return false;
1919
1920
// Open on click
1921
bool hovered, held;
1922
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1923
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1924
bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1925
if (pressed && !popup_open)
1926
{
1927
OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1928
popup_open = true;
1929
}
1930
1931
// Render shape
1932
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1933
const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1934
RenderNavCursor(bb, id);
1935
if (!(flags & ImGuiComboFlags_NoPreview))
1936
window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1937
if (!(flags & ImGuiComboFlags_NoArrowButton))
1938
{
1939
ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1940
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1941
window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1942
if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1943
RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1944
}
1945
RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1946
1947
// Custom preview
1948
if (flags & ImGuiComboFlags_CustomPreview)
1949
{
1950
g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1951
IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1952
preview_value = NULL;
1953
}
1954
1955
// Render preview and label
1956
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1957
{
1958
if (g.LogEnabled)
1959
LogSetNextTextDecoration("{", "}");
1960
RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
1961
}
1962
if (label_size.x > 0)
1963
RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
1964
1965
if (!popup_open)
1966
return false;
1967
1968
g.NextWindowData.HasFlags = backup_next_window_data_flags;
1969
return BeginComboPopup(popup_id, bb, flags);
1970
}
1971
1972
bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1973
{
1974
ImGuiContext& g = *GImGui;
1975
if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
1976
{
1977
g.NextWindowData.ClearFlags();
1978
return false;
1979
}
1980
1981
// Set popup size
1982
float w = bb.GetWidth();
1983
if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1984
{
1985
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
1986
}
1987
else
1988
{
1989
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1990
flags |= ImGuiComboFlags_HeightRegular;
1991
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1992
int popup_max_height_in_items = -1;
1993
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1994
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1995
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1996
ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1997
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1998
constraint_min.x = w;
1999
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
2000
constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
2001
SetNextWindowSizeConstraints(constraint_min, constraint_max);
2002
}
2003
2004
// This is essentially a specialized version of BeginPopupEx()
2005
char name[16];
2006
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
2007
2008
// Set position given a custom constraint (peak into expected window size so we can position it)
2009
// FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
2010
// FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
2011
if (ImGuiWindow* popup_window = FindWindowByName(name))
2012
if (popup_window->WasActive)
2013
{
2014
// Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
2015
ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
2016
popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
2017
ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
2018
ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
2019
SetNextWindowPos(pos);
2020
}
2021
2022
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
2023
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
2024
PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
2025
bool ret = Begin(name, NULL, window_flags);
2026
PopStyleVar();
2027
if (!ret)
2028
{
2029
EndPopup();
2030
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
2031
return false;
2032
}
2033
g.BeginComboDepth++;
2034
return true;
2035
}
2036
2037
void ImGui::EndCombo()
2038
{
2039
ImGuiContext& g = *GImGui;
2040
EndPopup();
2041
g.BeginComboDepth--;
2042
}
2043
2044
// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2045
// (Experimental, see GitHub issues: #1658, #4168)
2046
bool ImGui::BeginComboPreview()
2047
{
2048
ImGuiContext& g = *GImGui;
2049
ImGuiWindow* window = g.CurrentWindow;
2050
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2051
2052
if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2053
return false;
2054
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?
2055
if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
2056
return false;
2057
2058
// FIXME: This could be contained in a PushWorkRect() api
2059
preview_data->BackupCursorPos = window->DC.CursorPos;
2060
preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2061
preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2062
preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2063
preview_data->BackupLayout = window->DC.LayoutType;
2064
window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2065
window->DC.CursorMaxPos = window->DC.CursorPos;
2066
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2067
window->DC.IsSameLine = false;
2068
PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2069
2070
return true;
2071
}
2072
2073
void ImGui::EndComboPreview()
2074
{
2075
ImGuiContext& g = *GImGui;
2076
ImGuiWindow* window = g.CurrentWindow;
2077
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2078
2079
// FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2080
ImDrawList* draw_list = window->DrawList;
2081
if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2082
if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2083
{
2084
draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2085
draw_list->_TryMergeDrawCmds();
2086
}
2087
PopClipRect();
2088
window->DC.CursorPos = preview_data->BackupCursorPos;
2089
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2090
window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2091
window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2092
window->DC.LayoutType = preview_data->BackupLayout;
2093
window->DC.IsSameLine = false;
2094
preview_data->PreviewRect = ImRect();
2095
}
2096
2097
// Getter for the old Combo() API: const char*[]
2098
static const char* Items_ArrayGetter(void* data, int idx)
2099
{
2100
const char* const* items = (const char* const*)data;
2101
return items[idx];
2102
}
2103
2104
// Getter for the old Combo() API: "item1\0item2\0item3\0"
2105
static const char* Items_SingleStringGetter(void* data, int idx)
2106
{
2107
const char* items_separated_by_zeros = (const char*)data;
2108
int items_count = 0;
2109
const char* p = items_separated_by_zeros;
2110
while (*p)
2111
{
2112
if (idx == items_count)
2113
break;
2114
p += ImStrlen(p) + 1;
2115
items_count++;
2116
}
2117
return *p ? p : NULL;
2118
}
2119
2120
// Old API, prefer using BeginCombo() nowadays if you can.
2121
bool 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)
2122
{
2123
ImGuiContext& g = *GImGui;
2124
2125
// Call the getter to obtain the preview string which is a parameter to BeginCombo()
2126
const char* preview_value = NULL;
2127
if (*current_item >= 0 && *current_item < items_count)
2128
preview_value = getter(user_data, *current_item);
2129
2130
// 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.
2131
if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2132
SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2133
2134
if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2135
return false;
2136
2137
// Display items
2138
bool value_changed = false;
2139
ImGuiListClipper clipper;
2140
clipper.Begin(items_count);
2141
clipper.IncludeItemByIndex(*current_item);
2142
while (clipper.Step())
2143
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2144
{
2145
const char* item_text = getter(user_data, i);
2146
if (item_text == NULL)
2147
item_text = "*Unknown item*";
2148
2149
PushID(i);
2150
const bool item_selected = (i == *current_item);
2151
if (Selectable(item_text, item_selected) && *current_item != i)
2152
{
2153
value_changed = true;
2154
*current_item = i;
2155
}
2156
if (item_selected)
2157
SetItemDefaultFocus();
2158
PopID();
2159
}
2160
2161
EndCombo();
2162
if (value_changed)
2163
MarkItemEdited(g.LastItemData.ID);
2164
2165
return value_changed;
2166
}
2167
2168
// Combo box helper allowing to pass an array of strings.
2169
bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2170
{
2171
const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2172
return value_changed;
2173
}
2174
2175
// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2176
bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2177
{
2178
int items_count = 0;
2179
const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2180
while (*p)
2181
{
2182
p += ImStrlen(p) + 1;
2183
items_count++;
2184
}
2185
bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2186
return value_changed;
2187
}
2188
2189
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2190
2191
struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2192
static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2193
{
2194
ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2195
const char* s = NULL;
2196
data->OldCallback(data->UserData, idx, &s);
2197
return s;
2198
}
2199
2200
bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items)
2201
{
2202
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2203
return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items);
2204
}
2205
bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items)
2206
{
2207
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2208
return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items);
2209
}
2210
2211
#endif
2212
2213
//-------------------------------------------------------------------------
2214
// [SECTION] Data Type and Data Formatting Helpers [Internal]
2215
//-------------------------------------------------------------------------
2216
// - DataTypeGetInfo()
2217
// - DataTypeFormatString()
2218
// - DataTypeApplyOp()
2219
// - DataTypeApplyFromText()
2220
// - DataTypeCompare()
2221
// - DataTypeClamp()
2222
// - GetMinimumStepAtDecimalPrecision
2223
// - RoundScalarWithFormat<>()
2224
//-------------------------------------------------------------------------
2225
2226
static const ImGuiDataTypeInfo GDataTypeInfo[] =
2227
{
2228
{ sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2229
{ sizeof(unsigned char), "U8", "%u", "%u" },
2230
{ sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2231
{ sizeof(unsigned short), "U16", "%u", "%u" },
2232
{ sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2233
{ sizeof(unsigned int), "U32", "%u", "%u" },
2234
#ifdef _MSC_VER
2235
{ sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2236
{ sizeof(ImU64), "U64", "%I64u","%I64u" },
2237
#else
2238
{ sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2239
{ sizeof(ImU64), "U64", "%llu", "%llu" },
2240
#endif
2241
{ sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2242
{ sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2243
{ sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2244
{ 0, "char*","%s", "%s" }, // ImGuiDataType_String
2245
};
2246
IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2247
2248
const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2249
{
2250
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2251
return &GDataTypeInfo[data_type];
2252
}
2253
2254
int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2255
{
2256
// Signedness doesn't matter when pushing integer arguments
2257
if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2258
return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2259
if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2260
return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2261
if (data_type == ImGuiDataType_Float)
2262
return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2263
if (data_type == ImGuiDataType_Double)
2264
return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2265
if (data_type == ImGuiDataType_S8)
2266
return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2267
if (data_type == ImGuiDataType_U8)
2268
return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2269
if (data_type == ImGuiDataType_S16)
2270
return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2271
if (data_type == ImGuiDataType_U16)
2272
return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2273
IM_ASSERT(0);
2274
return 0;
2275
}
2276
2277
void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2278
{
2279
IM_ASSERT(op == '+' || op == '-');
2280
switch (data_type)
2281
{
2282
case ImGuiDataType_S8:
2283
if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2284
if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2285
return;
2286
case ImGuiDataType_U8:
2287
if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2288
if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2289
return;
2290
case ImGuiDataType_S16:
2291
if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2292
if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2293
return;
2294
case ImGuiDataType_U16:
2295
if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2296
if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2297
return;
2298
case ImGuiDataType_S32:
2299
if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2300
if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2301
return;
2302
case ImGuiDataType_U32:
2303
if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2304
if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2305
return;
2306
case ImGuiDataType_S64:
2307
if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2308
if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2309
return;
2310
case ImGuiDataType_U64:
2311
if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2312
if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2313
return;
2314
case ImGuiDataType_Float:
2315
if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2316
if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2317
return;
2318
case ImGuiDataType_Double:
2319
if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2320
if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2321
return;
2322
case ImGuiDataType_COUNT: break;
2323
}
2324
IM_ASSERT(0);
2325
}
2326
2327
// User can input math operators (e.g. +100) to edit a numerical values.
2328
// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2329
bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2330
{
2331
// Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2332
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2333
ImGuiDataTypeStorage data_backup;
2334
memcpy(&data_backup, p_data, type_info->Size);
2335
2336
while (ImCharIsBlankA(*buf))
2337
buf++;
2338
if (!buf[0])
2339
{
2340
if (p_data_when_empty != NULL)
2341
{
2342
memcpy(p_data, p_data_when_empty, type_info->Size);
2343
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2344
}
2345
return false;
2346
}
2347
2348
// Sanitize format
2349
// - 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
2350
// - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2351
char format_sanitized[32];
2352
if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2353
format = type_info->ScanFmt;
2354
else
2355
format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized));
2356
2357
// Small types need a 32-bit buffer to receive the result from scanf()
2358
int v32 = 0;
2359
if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2360
return false;
2361
if (type_info->Size < 4)
2362
{
2363
if (data_type == ImGuiDataType_S8)
2364
*(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2365
else if (data_type == ImGuiDataType_U8)
2366
*(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2367
else if (data_type == ImGuiDataType_S16)
2368
*(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2369
else if (data_type == ImGuiDataType_U16)
2370
*(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2371
else
2372
IM_ASSERT(0);
2373
}
2374
2375
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2376
}
2377
2378
template<typename T>
2379
static int DataTypeCompareT(const T* lhs, const T* rhs)
2380
{
2381
if (*lhs < *rhs) return -1;
2382
if (*lhs > *rhs) return +1;
2383
return 0;
2384
}
2385
2386
int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2387
{
2388
switch (data_type)
2389
{
2390
case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2391
case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2392
case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2393
case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2394
case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2395
case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2396
case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2397
case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2398
case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2399
case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2400
case ImGuiDataType_COUNT: break;
2401
}
2402
IM_ASSERT(0);
2403
return 0;
2404
}
2405
2406
template<typename T>
2407
static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2408
{
2409
// Clamp, both sides are optional, return true if modified
2410
if (v_min && *v < *v_min) { *v = *v_min; return true; }
2411
if (v_max && *v > *v_max) { *v = *v_max; return true; }
2412
return false;
2413
}
2414
2415
bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2416
{
2417
switch (data_type)
2418
{
2419
case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2420
case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2421
case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2422
case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2423
case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2424
case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2425
case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2426
case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2427
case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2428
case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2429
case ImGuiDataType_COUNT: break;
2430
}
2431
IM_ASSERT(0);
2432
return false;
2433
}
2434
2435
bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2436
{
2437
ImGuiContext& g = *GImGui;
2438
return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2439
}
2440
2441
static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2442
{
2443
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 };
2444
if (decimal_precision < 0)
2445
return FLT_MIN;
2446
return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2447
}
2448
2449
template<typename TYPE>
2450
TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2451
{
2452
IM_UNUSED(data_type);
2453
IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2454
const char* fmt_start = ImParseFormatFindStart(format);
2455
if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2456
return v;
2457
2458
// Sanitize format
2459
char fmt_sanitized[32];
2460
ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2461
fmt_start = fmt_sanitized;
2462
2463
// Format value with our rounding, and read back
2464
char v_str[64];
2465
ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2466
const char* p = v_str;
2467
while (*p == ' ')
2468
p++;
2469
v = (TYPE)ImAtof(p);
2470
2471
return v;
2472
}
2473
2474
//-------------------------------------------------------------------------
2475
// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2476
//-------------------------------------------------------------------------
2477
// - DragBehaviorT<>() [Internal]
2478
// - DragBehavior() [Internal]
2479
// - DragScalar()
2480
// - DragScalarN()
2481
// - DragFloat()
2482
// - DragFloat2()
2483
// - DragFloat3()
2484
// - DragFloat4()
2485
// - DragFloatRange2()
2486
// - DragInt()
2487
// - DragInt2()
2488
// - DragInt3()
2489
// - DragInt4()
2490
// - DragIntRange2()
2491
//-------------------------------------------------------------------------
2492
2493
// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2494
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2495
bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2496
{
2497
ImGuiContext& g = *GImGui;
2498
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2499
const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2500
const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2501
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2502
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2503
2504
// Default tweak speed
2505
if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2506
v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2507
2508
// Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2509
float adjust_delta = 0.0f;
2510
if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2511
{
2512
adjust_delta = g.IO.MouseDelta[axis];
2513
if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2514
adjust_delta *= 1.0f / 100.0f;
2515
if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2516
adjust_delta *= 10.0f;
2517
}
2518
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2519
{
2520
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2521
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2522
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2523
const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2524
adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2525
v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2526
}
2527
adjust_delta *= v_speed;
2528
2529
// For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2530
if (axis == ImGuiAxis_Y)
2531
adjust_delta = -adjust_delta;
2532
2533
// For logarithmic use our range is effectively 0..1 so scale the delta into that range
2534
if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2535
adjust_delta /= (float)(v_max - v_min);
2536
2537
// Clear current value on activation
2538
// 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.
2539
const bool is_just_activated = g.ActiveIdIsJustActivated;
2540
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));
2541
if (is_just_activated || is_already_past_limits_and_pushing_outward)
2542
{
2543
g.DragCurrentAccum = 0.0f;
2544
g.DragCurrentAccumDirty = false;
2545
}
2546
else if (adjust_delta != 0.0f)
2547
{
2548
g.DragCurrentAccum += adjust_delta;
2549
g.DragCurrentAccumDirty = true;
2550
}
2551
2552
if (!g.DragCurrentAccumDirty)
2553
return false;
2554
2555
TYPE v_cur = *v;
2556
FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2557
2558
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2559
const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2560
if (is_logarithmic)
2561
{
2562
// 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.
2563
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2564
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2565
2566
// Convert to parametric space, apply delta, convert back
2567
float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2568
float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2569
v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2570
v_old_ref_for_accum_remainder = v_old_parametric;
2571
}
2572
else
2573
{
2574
v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2575
}
2576
2577
// Round to user desired precision based on format string
2578
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2579
v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2580
2581
// Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2582
g.DragCurrentAccumDirty = false;
2583
if (is_logarithmic)
2584
{
2585
// Convert to parametric space, apply delta, convert back
2586
float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2587
g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2588
}
2589
else
2590
{
2591
g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2592
}
2593
2594
// Lose zero sign for float/double
2595
if (v_cur == (TYPE)-0)
2596
v_cur = (TYPE)0;
2597
2598
if (*v != v_cur && is_bounded)
2599
{
2600
if (is_wrapped)
2601
{
2602
// Wrap values
2603
if (v_cur < v_min)
2604
v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2605
if (v_cur > v_max)
2606
v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2607
}
2608
else
2609
{
2610
// Clamp values + handle overflow/wrap-around for integer types.
2611
if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2612
v_cur = v_min;
2613
if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2614
v_cur = v_max;
2615
}
2616
}
2617
2618
// Apply result
2619
if (*v == v_cur)
2620
return false;
2621
*v = v_cur;
2622
return true;
2623
}
2624
2625
bool 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)
2626
{
2627
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2628
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.");
2629
2630
ImGuiContext& g = *GImGui;
2631
if (g.ActiveId == id)
2632
{
2633
// Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2634
if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2635
ClearActiveID();
2636
else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2637
ClearActiveID();
2638
}
2639
if (g.ActiveId != id)
2640
return false;
2641
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2642
return false;
2643
2644
switch (data_type)
2645
{
2646
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; }
2647
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; }
2648
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; }
2649
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; }
2650
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);
2651
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);
2652
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);
2653
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);
2654
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);
2655
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);
2656
case ImGuiDataType_COUNT: break;
2657
}
2658
IM_ASSERT(0);
2659
return false;
2660
}
2661
2662
// 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.
2663
// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2664
bool 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)
2665
{
2666
ImGuiWindow* window = GetCurrentWindow();
2667
if (window->SkipItems)
2668
return false;
2669
2670
ImGuiContext& g = *GImGui;
2671
const ImGuiStyle& style = g.Style;
2672
const ImGuiID id = window->GetID(label);
2673
const float w = CalcItemWidth();
2674
2675
const ImVec2 label_size = CalcTextSize(label, NULL, true);
2676
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2677
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));
2678
2679
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2680
ItemSize(total_bb, style.FramePadding.y);
2681
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2682
return false;
2683
2684
// Default format string when passing NULL
2685
if (format == NULL)
2686
format = DataTypeGetInfo(data_type)->PrintFmt;
2687
2688
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2689
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2690
if (!temp_input_is_active)
2691
{
2692
// Tabbing or CTRL+click on Drag turns it into an InputText
2693
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2694
const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2695
const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2696
if (make_active && (clicked || double_clicked))
2697
SetKeyOwner(ImGuiKey_MouseLeft, id);
2698
if (make_active && temp_input_allowed)
2699
if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2700
temp_input_is_active = true;
2701
2702
// (Optional) simple click (without moving) turns Drag into an InputText
2703
if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2704
if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2705
{
2706
g.NavActivateId = id;
2707
g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2708
temp_input_is_active = true;
2709
}
2710
2711
// Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2712
if (make_active)
2713
memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
2714
2715
if (make_active && !temp_input_is_active)
2716
{
2717
SetActiveID(id, window);
2718
SetFocusID(id, window);
2719
FocusWindow(window);
2720
g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2721
}
2722
}
2723
2724
if (temp_input_is_active)
2725
{
2726
// Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2727
bool clamp_enabled = false;
2728
if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2729
{
2730
const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max
2731
if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2732
clamp_enabled = true;
2733
else if (clamp_range_dir == 0)
2734
clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2735
}
2736
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2737
}
2738
2739
// Draw frame
2740
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2741
RenderNavCursor(frame_bb, id);
2742
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding);
2743
2744
// Drag behavior
2745
const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2746
if (value_changed)
2747
MarkItemEdited(id);
2748
2749
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2750
char value_buf[64];
2751
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2752
if (g.LogEnabled)
2753
LogSetNextTextDecoration("{", "}");
2754
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2755
2756
if (label_size.x > 0.0f)
2757
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
2758
2759
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2760
return value_changed;
2761
}
2762
2763
bool 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)
2764
{
2765
ImGuiWindow* window = GetCurrentWindow();
2766
if (window->SkipItems)
2767
return false;
2768
2769
ImGuiContext& g = *GImGui;
2770
bool value_changed = false;
2771
BeginGroup();
2772
PushID(label);
2773
PushMultiItemsWidths(components, CalcItemWidth());
2774
size_t type_size = GDataTypeInfo[data_type].Size;
2775
for (int i = 0; i < components; i++)
2776
{
2777
PushID(i);
2778
if (i > 0)
2779
SameLine(0, g.Style.ItemInnerSpacing.x);
2780
value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2781
PopID();
2782
PopItemWidth();
2783
p_data = (void*)((char*)p_data + type_size);
2784
}
2785
PopID();
2786
2787
const char* label_end = FindRenderedTextEnd(label);
2788
if (label != label_end)
2789
{
2790
SameLine(0, g.Style.ItemInnerSpacing.x);
2791
TextEx(label, label_end);
2792
}
2793
2794
EndGroup();
2795
return value_changed;
2796
}
2797
2798
bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2799
{
2800
return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2801
}
2802
2803
bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2804
{
2805
return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2806
}
2807
2808
bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2809
{
2810
return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2811
}
2812
2813
bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2814
{
2815
return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2816
}
2817
2818
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2819
bool 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)
2820
{
2821
ImGuiWindow* window = GetCurrentWindow();
2822
if (window->SkipItems)
2823
return false;
2824
2825
ImGuiContext& g = *GImGui;
2826
PushID(label);
2827
BeginGroup();
2828
PushMultiItemsWidths(2, CalcItemWidth());
2829
2830
float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2831
float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2832
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2833
bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2834
PopItemWidth();
2835
SameLine(0, g.Style.ItemInnerSpacing.x);
2836
2837
float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2838
float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2839
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2840
value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2841
PopItemWidth();
2842
SameLine(0, g.Style.ItemInnerSpacing.x);
2843
2844
TextEx(label, FindRenderedTextEnd(label));
2845
EndGroup();
2846
PopID();
2847
2848
return value_changed;
2849
}
2850
2851
// NB: v_speed is float to allow adjusting the drag speed with more precision
2852
bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2853
{
2854
return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2855
}
2856
2857
bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2858
{
2859
return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2860
}
2861
2862
bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2863
{
2864
return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2865
}
2866
2867
bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2868
{
2869
return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2870
}
2871
2872
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2873
bool 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)
2874
{
2875
ImGuiWindow* window = GetCurrentWindow();
2876
if (window->SkipItems)
2877
return false;
2878
2879
ImGuiContext& g = *GImGui;
2880
PushID(label);
2881
BeginGroup();
2882
PushMultiItemsWidths(2, CalcItemWidth());
2883
2884
int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2885
int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2886
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2887
bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2888
PopItemWidth();
2889
SameLine(0, g.Style.ItemInnerSpacing.x);
2890
2891
int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2892
int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2893
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2894
value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2895
PopItemWidth();
2896
SameLine(0, g.Style.ItemInnerSpacing.x);
2897
2898
TextEx(label, FindRenderedTextEnd(label));
2899
EndGroup();
2900
PopID();
2901
2902
return value_changed;
2903
}
2904
2905
//-------------------------------------------------------------------------
2906
// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2907
//-------------------------------------------------------------------------
2908
// - ScaleRatioFromValueT<> [Internal]
2909
// - ScaleValueFromRatioT<> [Internal]
2910
// - SliderBehaviorT<>() [Internal]
2911
// - SliderBehavior() [Internal]
2912
// - SliderScalar()
2913
// - SliderScalarN()
2914
// - SliderFloat()
2915
// - SliderFloat2()
2916
// - SliderFloat3()
2917
// - SliderFloat4()
2918
// - SliderAngle()
2919
// - SliderInt()
2920
// - SliderInt2()
2921
// - SliderInt3()
2922
// - SliderInt4()
2923
// - VSliderScalar()
2924
// - VSliderFloat()
2925
// - VSliderInt()
2926
//-------------------------------------------------------------------------
2927
2928
// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2929
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2930
float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2931
{
2932
if (v_min == v_max)
2933
return 0.0f;
2934
IM_UNUSED(data_type);
2935
2936
const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2937
if (is_logarithmic)
2938
{
2939
bool flipped = v_max < v_min;
2940
2941
if (flipped) // Handle the case where the range is backwards
2942
ImSwap(v_min, v_max);
2943
2944
// Fudge min/max to avoid getting close to log(0)
2945
FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2946
FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2947
2948
// Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2949
if ((v_min == 0.0f) && (v_max < 0.0f))
2950
v_min_fudged = -logarithmic_zero_epsilon;
2951
else if ((v_max == 0.0f) && (v_min < 0.0f))
2952
v_max_fudged = -logarithmic_zero_epsilon;
2953
2954
float result;
2955
if (v_clamped <= v_min_fudged)
2956
result = 0.0f; // Workaround for values that are in-range but below our fudge
2957
else if (v_clamped >= v_max_fudged)
2958
result = 1.0f; // Workaround for values that are in-range but above our fudge
2959
else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2960
{
2961
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)
2962
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2963
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2964
if (v == 0.0f)
2965
result = zero_point_center; // Special case for exactly zero
2966
else if (v < 0.0f)
2967
result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2968
else
2969
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));
2970
}
2971
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2972
result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2973
else
2974
result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2975
2976
return flipped ? (1.0f - result) : result;
2977
}
2978
else
2979
{
2980
// Linear slider
2981
return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2982
}
2983
}
2984
2985
// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2986
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2987
TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize)
2988
{
2989
// We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2990
// but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2991
if (t <= 0.0f || v_min == v_max)
2992
return v_min;
2993
if (t >= 1.0f)
2994
return v_max;
2995
2996
TYPE result = (TYPE)0;
2997
if (is_logarithmic)
2998
{
2999
// Fudge min/max to avoid getting silly results close to zero
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
const bool flipped = v_max < v_min; // Check if range is "backwards"
3004
if (flipped)
3005
ImSwap(v_min_fudged, v_max_fudged);
3006
3007
// Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3008
if ((v_max == 0.0f) && (v_min < 0.0f))
3009
v_max_fudged = -logarithmic_zero_epsilon;
3010
3011
float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
3012
3013
if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
3014
{
3015
float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
3016
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3017
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3018
if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
3019
result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
3020
else if (t_with_flip < zero_point_center)
3021
result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
3022
else
3023
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))));
3024
}
3025
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3026
result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
3027
else
3028
result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
3029
}
3030
else
3031
{
3032
// Linear slider
3033
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3034
if (is_floating_point)
3035
{
3036
result = ImLerp(v_min, v_max, t);
3037
}
3038
else if (t < 1.0)
3039
{
3040
// - For integer values we want the clicking position to match the grab box so we round above
3041
// This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3042
// - 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
3043
// range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3044
FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3045
result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3046
}
3047
}
3048
3049
return result;
3050
}
3051
3052
// FIXME: Try to move more of the code into shared SliderBehavior()
3053
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3054
bool 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)
3055
{
3056
ImGuiContext& g = *GImGui;
3057
const ImGuiStyle& style = g.Style;
3058
3059
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3060
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3061
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3062
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.
3063
3064
// Calculate bounds
3065
const float grab_padding = 2.0f; // FIXME: Should be part of style.
3066
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3067
float grab_sz = style.GrabMinSize;
3068
if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3069
grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3070
grab_sz = ImMin(grab_sz, slider_sz);
3071
const float slider_usable_sz = slider_sz - grab_sz;
3072
const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3073
const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3074
3075
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3076
float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3077
if (is_logarithmic)
3078
{
3079
// 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.
3080
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3081
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3082
zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3083
}
3084
3085
// Process interacting with the slider
3086
bool value_changed = false;
3087
if (g.ActiveId == id)
3088
{
3089
bool set_new_value = false;
3090
float clicked_t = 0.0f;
3091
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3092
{
3093
if (!g.IO.MouseDown[0])
3094
{
3095
ClearActiveID();
3096
}
3097
else
3098
{
3099
const float mouse_abs_pos = g.IO.MousePos[axis];
3100
if (g.ActiveIdIsJustActivated)
3101
{
3102
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3103
if (axis == ImGuiAxis_Y)
3104
grab_t = 1.0f - grab_t;
3105
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3106
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.
3107
g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3108
}
3109
if (slider_usable_sz > 0.0f)
3110
clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3111
if (axis == ImGuiAxis_Y)
3112
clicked_t = 1.0f - clicked_t;
3113
set_new_value = true;
3114
}
3115
}
3116
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3117
{
3118
if (g.ActiveIdIsJustActivated)
3119
{
3120
g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3121
g.SliderCurrentAccumDirty = false;
3122
}
3123
3124
float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3125
if (input_delta != 0.0f)
3126
{
3127
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3128
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3129
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3130
if (decimal_precision > 0)
3131
{
3132
input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3133
if (tweak_slow)
3134
input_delta /= 10.0f;
3135
}
3136
else
3137
{
3138
if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3139
input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3140
else
3141
input_delta /= 100.0f;
3142
}
3143
if (tweak_fast)
3144
input_delta *= 10.0f;
3145
3146
g.SliderCurrentAccum += input_delta;
3147
g.SliderCurrentAccumDirty = true;
3148
}
3149
3150
float delta = g.SliderCurrentAccum;
3151
if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3152
{
3153
ClearActiveID();
3154
}
3155
else if (g.SliderCurrentAccumDirty)
3156
{
3157
clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3158
3159
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
3160
{
3161
set_new_value = false;
3162
g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3163
}
3164
else
3165
{
3166
set_new_value = true;
3167
float old_clicked_t = clicked_t;
3168
clicked_t = ImSaturate(clicked_t + delta);
3169
3170
// Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3171
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3172
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3173
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3174
float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3175
3176
if (delta > 0)
3177
g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3178
else
3179
g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3180
}
3181
3182
g.SliderCurrentAccumDirty = false;
3183
}
3184
}
3185
3186
if (set_new_value)
3187
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3188
set_new_value = false;
3189
3190
if (set_new_value)
3191
{
3192
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3193
3194
// Round to user desired precision based on format string
3195
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3196
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3197
3198
// Apply result
3199
if (*v != v_new)
3200
{
3201
*v = v_new;
3202
value_changed = true;
3203
}
3204
}
3205
}
3206
3207
if (slider_sz < 1.0f)
3208
{
3209
*out_grab_bb = ImRect(bb.Min, bb.Min);
3210
}
3211
else
3212
{
3213
// Output grab position so it can be displayed by the caller
3214
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3215
if (axis == ImGuiAxis_Y)
3216
grab_t = 1.0f - grab_t;
3217
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3218
if (axis == ImGuiAxis_X)
3219
*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);
3220
else
3221
*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);
3222
}
3223
3224
return value_changed;
3225
}
3226
3227
// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3228
// 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.
3229
// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3230
bool 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)
3231
{
3232
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3233
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.");
3234
IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3235
3236
switch (data_type)
3237
{
3238
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; }
3239
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; }
3240
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; }
3241
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; }
3242
case ImGuiDataType_S32:
3243
IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3244
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);
3245
case ImGuiDataType_U32:
3246
IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3247
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);
3248
case ImGuiDataType_S64:
3249
IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3250
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);
3251
case ImGuiDataType_U64:
3252
IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3253
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);
3254
case ImGuiDataType_Float:
3255
IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3256
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);
3257
case ImGuiDataType_Double:
3258
IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3259
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);
3260
case ImGuiDataType_COUNT: break;
3261
}
3262
IM_ASSERT(0);
3263
return false;
3264
}
3265
3266
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3267
// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3268
bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags)
3269
{
3270
ImGuiWindow* window = GetCurrentWindow();
3271
if (window->SkipItems)
3272
return false;
3273
3274
ImGuiContext& g = *GImGui;
3275
const ImGuiStyle& style = g.Style;
3276
const ImGuiID id = window->GetID(label);
3277
const float w = CalcItemWidth();
3278
3279
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3280
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3281
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));
3282
3283
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3284
ItemSize(total_bb, style.FramePadding.y);
3285
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3286
return false;
3287
3288
// Default format string when passing NULL
3289
if (format == NULL)
3290
format = DataTypeGetInfo(data_type)->PrintFmt;
3291
3292
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3293
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3294
if (!temp_input_is_active)
3295
{
3296
// Tabbing or CTRL+click on Slider turns it into an input box
3297
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3298
const bool make_active = (clicked || g.NavActivateId == id);
3299
if (make_active && clicked)
3300
SetKeyOwner(ImGuiKey_MouseLeft, id);
3301
if (make_active && temp_input_allowed)
3302
if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3303
temp_input_is_active = true;
3304
3305
// Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3306
if (make_active)
3307
memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
3308
3309
if (make_active && !temp_input_is_active)
3310
{
3311
SetActiveID(id, window);
3312
SetFocusID(id, window);
3313
FocusWindow(window);
3314
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3315
}
3316
}
3317
3318
if (temp_input_is_active)
3319
{
3320
// Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3321
const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3322
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3323
}
3324
3325
// Draw frame
3326
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3327
RenderNavCursor(frame_bb, id);
3328
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3329
3330
// Slider behavior
3331
ImRect grab_bb;
3332
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3333
if (value_changed)
3334
MarkItemEdited(id);
3335
3336
// Render grab
3337
if (grab_bb.Max.x > grab_bb.Min.x)
3338
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3339
3340
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3341
char value_buf[64];
3342
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3343
if (g.LogEnabled)
3344
LogSetNextTextDecoration("{", "}");
3345
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3346
3347
if (label_size.x > 0.0f)
3348
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3349
3350
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3351
return value_changed;
3352
}
3353
3354
// Add multiple sliders on 1 line for compact edition of multiple components
3355
bool 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)
3356
{
3357
ImGuiWindow* window = GetCurrentWindow();
3358
if (window->SkipItems)
3359
return false;
3360
3361
ImGuiContext& g = *GImGui;
3362
bool value_changed = false;
3363
BeginGroup();
3364
PushID(label);
3365
PushMultiItemsWidths(components, CalcItemWidth());
3366
size_t type_size = GDataTypeInfo[data_type].Size;
3367
for (int i = 0; i < components; i++)
3368
{
3369
PushID(i);
3370
if (i > 0)
3371
SameLine(0, g.Style.ItemInnerSpacing.x);
3372
value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3373
PopID();
3374
PopItemWidth();
3375
v = (void*)((char*)v + type_size);
3376
}
3377
PopID();
3378
3379
const char* label_end = FindRenderedTextEnd(label);
3380
if (label != label_end)
3381
{
3382
SameLine(0, g.Style.ItemInnerSpacing.x);
3383
TextEx(label, label_end);
3384
}
3385
3386
EndGroup();
3387
return value_changed;
3388
}
3389
3390
bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3391
{
3392
return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3393
}
3394
3395
bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3396
{
3397
return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3398
}
3399
3400
bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3401
{
3402
return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3403
}
3404
3405
bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3406
{
3407
return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3408
}
3409
3410
bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3411
{
3412
if (format == NULL)
3413
format = "%.0f deg";
3414
float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3415
bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3416
if (value_changed)
3417
*v_rad = v_deg * (2 * IM_PI) / 360.0f;
3418
return value_changed;
3419
}
3420
3421
bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3422
{
3423
return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3424
}
3425
3426
bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3427
{
3428
return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3429
}
3430
3431
bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3432
{
3433
return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3434
}
3435
3436
bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3437
{
3438
return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3439
}
3440
3441
bool 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)
3442
{
3443
ImGuiWindow* window = GetCurrentWindow();
3444
if (window->SkipItems)
3445
return false;
3446
3447
ImGuiContext& g = *GImGui;
3448
const ImGuiStyle& style = g.Style;
3449
const ImGuiID id = window->GetID(label);
3450
3451
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3452
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3453
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));
3454
3455
ItemSize(bb, style.FramePadding.y);
3456
if (!ItemAdd(frame_bb, id))
3457
return false;
3458
3459
// Default format string when passing NULL
3460
if (format == NULL)
3461
format = DataTypeGetInfo(data_type)->PrintFmt;
3462
3463
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3464
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3465
if (clicked || g.NavActivateId == id)
3466
{
3467
if (clicked)
3468
SetKeyOwner(ImGuiKey_MouseLeft, id);
3469
SetActiveID(id, window);
3470
SetFocusID(id, window);
3471
FocusWindow(window);
3472
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3473
}
3474
3475
// Draw frame
3476
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3477
RenderNavCursor(frame_bb, id);
3478
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3479
3480
// Slider behavior
3481
ImRect grab_bb;
3482
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3483
if (value_changed)
3484
MarkItemEdited(id);
3485
3486
// Render grab
3487
if (grab_bb.Max.y > grab_bb.Min.y)
3488
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3489
3490
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3491
// For the vertical slider we allow centered text to overlap the frame padding
3492
char value_buf[64];
3493
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3494
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));
3495
if (label_size.x > 0.0f)
3496
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3497
3498
return value_changed;
3499
}
3500
3501
bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3502
{
3503
return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3504
}
3505
3506
bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3507
{
3508
return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3509
}
3510
3511
//-------------------------------------------------------------------------
3512
// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3513
//-------------------------------------------------------------------------
3514
// - ImParseFormatFindStart() [Internal]
3515
// - ImParseFormatFindEnd() [Internal]
3516
// - ImParseFormatTrimDecorations() [Internal]
3517
// - ImParseFormatSanitizeForPrinting() [Internal]
3518
// - ImParseFormatSanitizeForScanning() [Internal]
3519
// - ImParseFormatPrecision() [Internal]
3520
// - TempInputTextScalar() [Internal]
3521
// - InputScalar()
3522
// - InputScalarN()
3523
// - InputFloat()
3524
// - InputFloat2()
3525
// - InputFloat3()
3526
// - InputFloat4()
3527
// - InputInt()
3528
// - InputInt2()
3529
// - InputInt3()
3530
// - InputInt4()
3531
// - InputDouble()
3532
//-------------------------------------------------------------------------
3533
3534
// We don't use strchr() because our strings are usually very short and often start with '%'
3535
const char* ImParseFormatFindStart(const char* fmt)
3536
{
3537
while (char c = fmt[0])
3538
{
3539
if (c == '%' && fmt[1] != '%')
3540
return fmt;
3541
else if (c == '%')
3542
fmt++;
3543
fmt++;
3544
}
3545
return fmt;
3546
}
3547
3548
const char* ImParseFormatFindEnd(const char* fmt)
3549
{
3550
// Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3551
if (fmt[0] != '%')
3552
return fmt;
3553
const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3554
const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3555
for (char c; (c = *fmt) != 0; fmt++)
3556
{
3557
if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3558
return fmt + 1;
3559
if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3560
return fmt + 1;
3561
}
3562
return fmt;
3563
}
3564
3565
// Extract the format out of a format string with leading or trailing decorations
3566
// fmt = "blah blah" -> return ""
3567
// fmt = "%.3f" -> return fmt
3568
// fmt = "hello %.3f" -> return fmt + 6
3569
// fmt = "%.3f hello" -> return buf written with "%.3f"
3570
const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3571
{
3572
const char* fmt_start = ImParseFormatFindStart(fmt);
3573
if (fmt_start[0] != '%')
3574
return "";
3575
const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3576
if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3577
return fmt_start;
3578
ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3579
return buf;
3580
}
3581
3582
// Sanitize format
3583
// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3584
// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3585
void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3586
{
3587
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3588
IM_UNUSED(fmt_out_size);
3589
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3590
while (fmt_in < fmt_end)
3591
{
3592
char c = *fmt_in++;
3593
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3594
*(fmt_out++) = c;
3595
}
3596
*fmt_out = 0; // Zero-terminate
3597
}
3598
3599
// - 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"
3600
const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3601
{
3602
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3603
const char* fmt_out_begin = fmt_out;
3604
IM_UNUSED(fmt_out_size);
3605
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3606
bool has_type = false;
3607
while (fmt_in < fmt_end)
3608
{
3609
char c = *fmt_in++;
3610
if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3611
continue;
3612
has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3613
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3614
*(fmt_out++) = c;
3615
}
3616
*fmt_out = 0; // Zero-terminate
3617
return fmt_out_begin;
3618
}
3619
3620
template<typename TYPE>
3621
static const char* ImAtoi(const char* src, TYPE* output)
3622
{
3623
int negative = 0;
3624
if (*src == '-') { negative = 1; src++; }
3625
if (*src == '+') { src++; }
3626
TYPE v = 0;
3627
while (*src >= '0' && *src <= '9')
3628
v = (v * 10) + (*src++ - '0');
3629
*output = negative ? -v : v;
3630
return src;
3631
}
3632
3633
// Parse display precision back from the display format string
3634
// 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.
3635
int ImParseFormatPrecision(const char* fmt, int default_precision)
3636
{
3637
fmt = ImParseFormatFindStart(fmt);
3638
if (fmt[0] != '%')
3639
return default_precision;
3640
fmt++;
3641
while (*fmt >= '0' && *fmt <= '9')
3642
fmt++;
3643
int precision = INT_MAX;
3644
if (*fmt == '.')
3645
{
3646
fmt = ImAtoi<int>(fmt + 1, &precision);
3647
if (precision < 0 || precision > 99)
3648
precision = default_precision;
3649
}
3650
if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3651
precision = -1;
3652
if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3653
precision = -1;
3654
return (precision == INT_MAX) ? default_precision : precision;
3655
}
3656
3657
// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3658
// FIXME: Facilitate using this in variety of other situations.
3659
// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3660
// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3661
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3662
{
3663
// On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3664
// We clear ActiveID on the first frame to allow the InputText() taking it back.
3665
ImGuiContext& g = *GImGui;
3666
const bool init = (g.TempInputId != id);
3667
if (init)
3668
ClearActiveID();
3669
3670
g.CurrentWindow->DC.CursorPos = bb.Min;
3671
g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3672
bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem);
3673
if (init)
3674
{
3675
// First frame we started displaying the InputText widget, we expect it to take the active id.
3676
IM_ASSERT(g.ActiveId == id);
3677
g.TempInputId = g.ActiveId;
3678
}
3679
return value_changed;
3680
}
3681
3682
// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3683
// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3684
// However this may not be ideal for all uses, as some user code may break on out of bound values.
3685
bool 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)
3686
{
3687
// FIXME: May need to clarify display behavior if format doesn't contain %.
3688
// "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3689
ImGuiContext& g = *GImGui;
3690
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3691
char fmt_buf[32];
3692
char data_buf[32];
3693
format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf));
3694
if (format[0] == 0)
3695
format = type_info->PrintFmt;
3696
DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3697
ImStrTrimBlanks(data_buf);
3698
3699
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3700
g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3701
bool value_changed = false;
3702
if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags))
3703
{
3704
// Backup old value
3705
size_t data_type_size = type_info->Size;
3706
ImGuiDataTypeStorage data_backup;
3707
memcpy(&data_backup, p_data, data_type_size);
3708
3709
// Apply new value (or operations) then clamp
3710
DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3711
if (p_clamp_min || p_clamp_max)
3712
{
3713
if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3714
ImSwap(p_clamp_min, p_clamp_max);
3715
DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3716
}
3717
3718
// Only mark as edited if new value is different
3719
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3720
value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3721
if (value_changed)
3722
MarkItemEdited(id);
3723
}
3724
return value_changed;
3725
}
3726
3727
void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3728
{
3729
ImGuiContext& g = *GImGui;
3730
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3731
memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3732
}
3733
3734
// 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.
3735
// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3736
bool 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)
3737
{
3738
ImGuiWindow* window = GetCurrentWindow();
3739
if (window->SkipItems)
3740
return false;
3741
3742
ImGuiContext& g = *GImGui;
3743
ImGuiStyle& style = g.Style;
3744
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()!
3745
3746
if (format == NULL)
3747
format = DataTypeGetInfo(data_type)->PrintFmt;
3748
3749
void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3750
3751
char buf[64];
3752
if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3753
buf[0] = 0;
3754
else
3755
DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3756
3757
// Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3758
// We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3759
g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3760
flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3761
3762
bool value_changed = false;
3763
if (p_step == NULL)
3764
{
3765
if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3766
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3767
}
3768
else
3769
{
3770
const float button_size = GetFrameHeight();
3771
3772
BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3773
PushID(label);
3774
SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3775
if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3776
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3777
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3778
3779
// Step buttons
3780
const ImVec2 backup_frame_padding = style.FramePadding;
3781
style.FramePadding.x = style.FramePadding.y;
3782
if (flags & ImGuiInputTextFlags_ReadOnly)
3783
BeginDisabled();
3784
PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3785
SameLine(0, style.ItemInnerSpacing.x);
3786
if (ButtonEx("-", ImVec2(button_size, button_size)))
3787
{
3788
DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3789
value_changed = true;
3790
}
3791
SameLine(0, style.ItemInnerSpacing.x);
3792
if (ButtonEx("+", ImVec2(button_size, button_size)))
3793
{
3794
DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3795
value_changed = true;
3796
}
3797
PopItemFlag();
3798
if (flags & ImGuiInputTextFlags_ReadOnly)
3799
EndDisabled();
3800
3801
const char* label_end = FindRenderedTextEnd(label);
3802
if (label != label_end)
3803
{
3804
SameLine(0, style.ItemInnerSpacing.x);
3805
TextEx(label, label_end);
3806
}
3807
style.FramePadding = backup_frame_padding;
3808
3809
PopID();
3810
EndGroup();
3811
}
3812
3813
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3814
if (value_changed)
3815
MarkItemEdited(g.LastItemData.ID);
3816
3817
return value_changed;
3818
}
3819
3820
bool 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)
3821
{
3822
ImGuiWindow* window = GetCurrentWindow();
3823
if (window->SkipItems)
3824
return false;
3825
3826
ImGuiContext& g = *GImGui;
3827
bool value_changed = false;
3828
BeginGroup();
3829
PushID(label);
3830
PushMultiItemsWidths(components, CalcItemWidth());
3831
size_t type_size = GDataTypeInfo[data_type].Size;
3832
for (int i = 0; i < components; i++)
3833
{
3834
PushID(i);
3835
if (i > 0)
3836
SameLine(0, g.Style.ItemInnerSpacing.x);
3837
value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3838
PopID();
3839
PopItemWidth();
3840
p_data = (void*)((char*)p_data + type_size);
3841
}
3842
PopID();
3843
3844
const char* label_end = FindRenderedTextEnd(label);
3845
if (label != label_end)
3846
{
3847
SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3848
TextEx(label, label_end);
3849
}
3850
3851
EndGroup();
3852
return value_changed;
3853
}
3854
3855
bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3856
{
3857
return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3858
}
3859
3860
bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3861
{
3862
return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3863
}
3864
3865
bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3866
{
3867
return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3868
}
3869
3870
bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3871
{
3872
return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3873
}
3874
3875
bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3876
{
3877
// 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.
3878
const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3879
return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3880
}
3881
3882
bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3883
{
3884
return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3885
}
3886
3887
bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3888
{
3889
return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3890
}
3891
3892
bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3893
{
3894
return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3895
}
3896
3897
bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3898
{
3899
return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3900
}
3901
3902
//-------------------------------------------------------------------------
3903
// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3904
//-------------------------------------------------------------------------
3905
// - imstb_textedit.h include
3906
// - InputText()
3907
// - InputTextWithHint()
3908
// - InputTextMultiline()
3909
// - InputTextGetCharInfo() [Internal]
3910
// - InputTextReindexLines() [Internal]
3911
// - InputTextReindexLinesRange() [Internal]
3912
// - InputTextEx() [Internal]
3913
// - DebugNodeInputTextState() [Internal]
3914
//-------------------------------------------------------------------------
3915
3916
namespace ImStb
3917
{
3918
#include "imstb_textedit.h"
3919
}
3920
3921
bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3922
{
3923
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3924
return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3925
}
3926
3927
bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3928
{
3929
return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3930
}
3931
3932
bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3933
{
3934
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3935
return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3936
}
3937
3938
// This is only used in the path where the multiline widget is inactive.
3939
static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3940
{
3941
int line_count = 0;
3942
const char* s = text_begin;
3943
while (true)
3944
{
3945
const char* s_eol = strchr(s, '\n');
3946
line_count++;
3947
if (s_eol == NULL)
3948
{
3949
s = s + ImStrlen(s);
3950
break;
3951
}
3952
s = s_eol + 1;
3953
}
3954
*out_text_end = s;
3955
return line_count;
3956
}
3957
3958
// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
3959
static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
3960
{
3961
ImGuiContext& g = *ctx;
3962
//ImFont* font = g.Font;
3963
ImFontBaked* baked = g.FontBaked;
3964
const float line_height = g.FontSize;
3965
const float scale = line_height / baked->Size;
3966
3967
ImVec2 text_size = ImVec2(0, 0);
3968
float line_width = 0.0f;
3969
3970
const char* s = text_begin;
3971
while (s < text_end)
3972
{
3973
unsigned int c = (unsigned int)*s;
3974
if (c < 0x80)
3975
s += 1;
3976
else
3977
s += ImTextCharFromUtf8(&c, s, text_end);
3978
3979
if (c == '\n')
3980
{
3981
text_size.x = ImMax(text_size.x, line_width);
3982
text_size.y += line_height;
3983
line_width = 0.0f;
3984
if (stop_on_new_line)
3985
break;
3986
continue;
3987
}
3988
if (c == '\r')
3989
continue;
3990
3991
line_width += baked->GetCharAdvance((ImWchar)c) * scale;
3992
}
3993
3994
if (text_size.x < line_width)
3995
text_size.x = line_width;
3996
3997
if (out_offset)
3998
*out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3999
4000
if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
4001
text_size.y += line_height;
4002
4003
if (remaining)
4004
*remaining = s;
4005
4006
return text_size;
4007
}
4008
4009
// 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)
4010
// With our UTF-8 use of stb_textedit:
4011
// - 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.
4012
// - 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).
4013
// - ...but we don't use that feature.
4014
namespace ImStb
4015
{
4016
static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
4017
static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; }
4018
static 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; }
4019
static char STB_TEXTEDIT_NEWLINE = '\n';
4020
static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
4021
{
4022
const char* text = obj->TextSrc;
4023
const char* text_remaining = NULL;
4024
const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true);
4025
r->x0 = 0.0f;
4026
r->x1 = size.x;
4027
r->baseline_y_delta = size.y;
4028
r->ymin = 0.0f;
4029
r->ymax = size.y;
4030
r->num_chars = (int)(text_remaining - (text + line_start_idx));
4031
}
4032
4033
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
4034
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
4035
4036
static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4037
{
4038
if (idx >= obj->TextLen)
4039
return obj->TextLen + 1;
4040
unsigned int c;
4041
return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
4042
}
4043
4044
static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4045
{
4046
if (idx <= 0)
4047
return -1;
4048
const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
4049
return (int)(p - obj->TextSrc);
4050
}
4051
4052
static bool ImCharIsSeparatorW(unsigned int c)
4053
{
4054
static const unsigned int separator_list[] =
4055
{
4056
',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4057
'[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4058
'\n', '\r',
4059
};
4060
for (unsigned int separator : separator_list)
4061
if (c == separator)
4062
return true;
4063
return false;
4064
}
4065
4066
static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4067
{
4068
// 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.
4069
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4070
return 0;
4071
4072
const char* curr_p = obj->TextSrc + idx;
4073
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4074
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
4075
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);
4076
4077
bool prev_white = ImCharIsBlankW(prev_c);
4078
bool prev_separ = ImCharIsSeparatorW(prev_c);
4079
bool curr_white = ImCharIsBlankW(curr_c);
4080
bool curr_separ = ImCharIsSeparatorW(curr_c);
4081
return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4082
}
4083
static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4084
{
4085
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4086
return 0;
4087
4088
const char* curr_p = obj->TextSrc + idx;
4089
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4090
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
4091
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);
4092
4093
bool prev_white = ImCharIsBlankW(prev_c);
4094
bool prev_separ = ImCharIsSeparatorW(prev_c);
4095
bool curr_white = ImCharIsBlankW(curr_c);
4096
bool curr_separ = ImCharIsSeparatorW(curr_c);
4097
return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4098
}
4099
static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4100
{
4101
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4102
while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4103
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4104
return idx < 0 ? 0 : idx;
4105
}
4106
static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4107
{
4108
int len = obj->TextLen;
4109
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4110
while (idx < len && !is_word_boundary_from_left(obj, idx))
4111
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4112
return idx > len ? len : idx;
4113
}
4114
static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4115
{
4116
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4117
int len = obj->TextLen;
4118
while (idx < len && !is_word_boundary_from_right(obj, idx))
4119
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4120
return idx > len ? len : idx;
4121
}
4122
static 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); }
4123
#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4124
#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4125
4126
static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4127
{
4128
// Offset remaining text (+ copy zero terminator)
4129
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4130
char* dst = obj->TextA.Data + pos;
4131
char* src = obj->TextA.Data + pos + n;
4132
memmove(dst, src, obj->TextLen - n - pos + 1);
4133
obj->Edited = true;
4134
obj->TextLen -= n;
4135
}
4136
4137
static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4138
{
4139
const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4140
const int text_len = obj->TextLen;
4141
IM_ASSERT(pos <= text_len);
4142
4143
if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity))
4144
return false;
4145
4146
// Grow internal buffer if needed
4147
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4148
if (new_text_len + text_len + 1 > obj->TextA.Size)
4149
{
4150
if (!is_resizable)
4151
return false;
4152
obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4153
obj->TextSrc = obj->TextA.Data;
4154
}
4155
4156
char* text = obj->TextA.Data;
4157
if (pos != text_len)
4158
memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4159
memcpy(text + pos, new_text, (size_t)new_text_len);
4160
4161
obj->Edited = true;
4162
obj->TextLen += new_text_len;
4163
obj->TextA[obj->TextLen] = '\0';
4164
4165
return true;
4166
}
4167
4168
// 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)
4169
#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4170
#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4171
#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4172
#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4173
#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4174
#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4175
#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4176
#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4177
#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4178
#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4179
#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4180
#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4181
#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4182
#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4183
#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4184
#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4185
#define STB_TEXTEDIT_K_SHIFT 0x400000
4186
4187
#define IMSTB_TEXTEDIT_IMPLEMENTATION
4188
#define IMSTB_TEXTEDIT_memmove memmove
4189
#include "imstb_textedit.h"
4190
4191
// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4192
// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4193
static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4194
{
4195
stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4196
ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4197
state->cursor = state->select_start = state->select_end = 0;
4198
if (text_len <= 0)
4199
return;
4200
if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len))
4201
{
4202
state->cursor = state->select_start = state->select_end = text_len;
4203
state->has_preferred_x = 0;
4204
return;
4205
}
4206
IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4207
}
4208
4209
} // namespace ImStb
4210
4211
// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4212
ImGuiInputTextState::ImGuiInputTextState()
4213
{
4214
memset(this, 0, sizeof(*this));
4215
Stb = IM_NEW(ImStbTexteditState);
4216
memset(Stb, 0, sizeof(*Stb));
4217
}
4218
4219
ImGuiInputTextState::~ImGuiInputTextState()
4220
{
4221
IM_DELETE(Stb);
4222
}
4223
4224
void ImGuiInputTextState::OnKeyPressed(int key)
4225
{
4226
stb_textedit_key(this, Stb, key);
4227
CursorFollow = true;
4228
CursorAnimReset();
4229
}
4230
4231
void ImGuiInputTextState::OnCharPressed(unsigned int c)
4232
{
4233
// Convert the key to a UTF8 byte sequence.
4234
// The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4235
char utf8[5];
4236
ImTextCharToUtf8(utf8, c);
4237
stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8));
4238
CursorFollow = true;
4239
CursorAnimReset();
4240
}
4241
4242
// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4243
void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4244
void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); }
4245
bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4246
void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4247
int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4248
int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4249
int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4250
void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4251
void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4252
void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4253
void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4254
4255
ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4256
{
4257
memset(this, 0, sizeof(*this));
4258
}
4259
4260
// Public API to manipulate UTF-8 text from within a callback.
4261
// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4262
// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4263
// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4264
void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4265
{
4266
IM_ASSERT(pos + bytes_count <= BufTextLen);
4267
char* dst = Buf + pos;
4268
const char* src = Buf + pos + bytes_count;
4269
memmove(dst, src, BufTextLen - bytes_count - pos + 1);
4270
4271
if (CursorPos >= pos + bytes_count)
4272
CursorPos -= bytes_count;
4273
else if (CursorPos >= pos)
4274
CursorPos = pos;
4275
SelectionStart = SelectionEnd = CursorPos;
4276
BufDirty = true;
4277
BufTextLen -= bytes_count;
4278
}
4279
4280
void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4281
{
4282
// Accept null ranges
4283
if (new_text == new_text_end)
4284
return;
4285
4286
ImGuiContext& g = *Ctx;
4287
ImGuiInputTextState* obj = &g.InputTextState;
4288
IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
4289
4290
// Grow internal buffer if needed
4291
const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4292
const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text);
4293
if (new_text_len + BufTextLen + 1 > obj->TextA.Size && (Flags & ImGuiInputTextFlags_ReadOnly) == 0)
4294
{
4295
if (!is_resizable)
4296
return;
4297
4298
IM_ASSERT(Buf == obj->TextA.Data);
4299
int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4300
obj->TextA.resize(new_buf_size + 1);
4301
obj->TextSrc = obj->TextA.Data;
4302
Buf = obj->TextA.Data;
4303
BufSize = obj->BufCapacity = new_buf_size;
4304
}
4305
4306
if (BufTextLen != pos)
4307
memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4308
memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4309
Buf[BufTextLen + new_text_len] = '\0';
4310
4311
if (CursorPos >= pos)
4312
CursorPos += new_text_len;
4313
SelectionStart = SelectionEnd = CursorPos;
4314
BufDirty = true;
4315
BufTextLen += new_text_len;
4316
}
4317
4318
void ImGui::PushPasswordFont()
4319
{
4320
ImGuiContext& g = *GImGui;
4321
ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4322
IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4323
ImFontGlyph* glyph = g.FontBaked->FindGlyph('*');
4324
g.InputTextPasswordFontBackupFlags = g.Font->Flags;
4325
backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
4326
backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
4327
backup->IndexLookup.swap(g.FontBaked->IndexLookup);
4328
backup->IndexAdvanceX.swap(g.FontBaked->IndexAdvanceX);
4329
g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
4330
g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(glyph);
4331
g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
4332
}
4333
4334
void ImGui::PopPasswordFont()
4335
{
4336
ImGuiContext& g = *GImGui;
4337
ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4338
g.Font->Flags = g.InputTextPasswordFontBackupFlags;
4339
g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
4340
g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
4341
g.FontBaked->IndexLookup.swap(backup->IndexLookup);
4342
g.FontBaked->IndexAdvanceX.swap(backup->IndexAdvanceX);
4343
IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4344
}
4345
4346
// Return false to discard a character.
4347
static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4348
{
4349
unsigned int c = *p_char;
4350
4351
// Filter non-printable (NB: isprint is unreliable! see #2467)
4352
bool apply_named_filters = true;
4353
if (c < 0x20)
4354
{
4355
bool pass = false;
4356
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)
4357
if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4358
{
4359
c = *p_char = ' ';
4360
pass = true;
4361
}
4362
pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4363
pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4364
if (!pass)
4365
return false;
4366
apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4367
}
4368
4369
if (input_source_is_clipboard == false)
4370
{
4371
// We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4372
if (c == 127)
4373
return false;
4374
4375
// Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4376
if (c >= 0xE000 && c <= 0xF8FF)
4377
return false;
4378
}
4379
4380
// Filter Unicode ranges we are not handling in this build
4381
if (c > IM_UNICODE_CODEPOINT_MAX)
4382
return false;
4383
4384
// Generic named filters
4385
if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4386
{
4387
// 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 '.'.
4388
// The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4389
// 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.
4390
// Change the default decimal_point with:
4391
// ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4392
// 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.
4393
ImGuiContext& g = *ctx;
4394
const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4395
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4396
if (c == '.' || c == ',')
4397
c = c_decimal_point;
4398
4399
// Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4400
// While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4401
// scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4402
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4403
if (c >= 0xFF01 && c <= 0xFF5E)
4404
c = c - 0xFF01 + 0x21;
4405
4406
// Allow 0-9 . - + * /
4407
if (flags & ImGuiInputTextFlags_CharsDecimal)
4408
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4409
return false;
4410
4411
// Allow 0-9 . - + * / e E
4412
if (flags & ImGuiInputTextFlags_CharsScientific)
4413
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4414
return false;
4415
4416
// Allow 0-9 a-F A-F
4417
if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4418
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4419
return false;
4420
4421
// Turn a-z into A-Z
4422
if (flags & ImGuiInputTextFlags_CharsUppercase)
4423
if (c >= 'a' && c <= 'z')
4424
c += (unsigned int)('A' - 'a');
4425
4426
if (flags & ImGuiInputTextFlags_CharsNoBlank)
4427
if (ImCharIsBlankW(c))
4428
return false;
4429
4430
*p_char = c;
4431
}
4432
4433
// Custom callback filter
4434
if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4435
{
4436
ImGuiContext& g = *GImGui;
4437
ImGuiInputTextCallbackData callback_data;
4438
callback_data.Ctx = &g;
4439
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4440
callback_data.EventChar = (ImWchar)c;
4441
callback_data.Flags = flags;
4442
callback_data.UserData = user_data;
4443
if (callback(&callback_data) != 0)
4444
return false;
4445
*p_char = callback_data.EventChar;
4446
if (!callback_data.EventChar)
4447
return false;
4448
}
4449
4450
return true;
4451
}
4452
4453
// Find the shortest single replacement we can make to get from old_buf to new_buf
4454
// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4455
// 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.
4456
static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4457
{
4458
const int shorter_length = ImMin(old_length, new_length);
4459
int first_diff;
4460
for (first_diff = 0; first_diff < shorter_length; first_diff++)
4461
if (old_buf[first_diff] != new_buf[first_diff])
4462
break;
4463
if (first_diff == old_length && first_diff == new_length)
4464
return;
4465
4466
int old_last_diff = old_length - 1;
4467
int new_last_diff = new_length - 1;
4468
for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4469
if (old_buf[old_last_diff] != new_buf[new_last_diff])
4470
break;
4471
4472
const int insert_len = new_last_diff - first_diff + 1;
4473
const int delete_len = old_last_diff - first_diff + 1;
4474
if (insert_len > 0 || delete_len > 0)
4475
if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4476
for (int i = 0; i < delete_len; i++)
4477
p[i] = old_buf[first_diff + i];
4478
}
4479
4480
// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4481
// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4482
// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4483
// but that more likely be attractive when we do have _NoLiveEdit flag available.
4484
void ImGui::InputTextDeactivateHook(ImGuiID id)
4485
{
4486
ImGuiContext& g = *GImGui;
4487
ImGuiInputTextState* state = &g.InputTextState;
4488
if (id == 0 || state->ID != id)
4489
return;
4490
g.InputTextDeactivatedState.ID = state->ID;
4491
if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4492
{
4493
g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4494
}
4495
else
4496
{
4497
IM_ASSERT(state->TextA.Data != 0);
4498
IM_ASSERT(state->TextA[state->TextLen] == 0);
4499
g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4500
memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4501
}
4502
}
4503
4504
// Edit a string of text
4505
// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4506
// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4507
// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4508
// - 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.
4509
// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4510
// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are
4511
// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188)
4512
bool 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)
4513
{
4514
ImGuiWindow* window = GetCurrentWindow();
4515
if (window->SkipItems)
4516
return false;
4517
4518
IM_ASSERT(buf != NULL && buf_size >= 0);
4519
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4520
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4521
IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming
4522
4523
ImGuiContext& g = *GImGui;
4524
ImGuiIO& io = g.IO;
4525
const ImGuiStyle& style = g.Style;
4526
4527
const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4528
const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4529
4530
if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4531
BeginGroup();
4532
const ImGuiID id = window->GetID(label);
4533
const ImVec2 label_size = CalcTextSize(label, NULL, true);
4534
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
4535
const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4536
4537
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4538
const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4539
4540
ImGuiWindow* draw_window = window;
4541
ImVec2 inner_size = frame_size;
4542
ImGuiLastItemData item_data_backup;
4543
if (is_multiline)
4544
{
4545
ImVec2 backup_pos = window->DC.CursorPos;
4546
ItemSize(total_bb, style.FramePadding.y);
4547
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4548
{
4549
EndGroup();
4550
return false;
4551
}
4552
item_data_backup = g.LastItemData;
4553
window->DC.CursorPos = backup_pos;
4554
4555
// Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4556
if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4557
g.NavActivateId = 0;
4558
4559
// Prevent NavActivate reactivating in BeginChild() when we are already active.
4560
const ImGuiID backup_activate_id = g.NavActivateId;
4561
if (g.ActiveId == id) // Prevent reactivation
4562
g.NavActivateId = 0;
4563
4564
// We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4565
PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4566
PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4567
PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4568
PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4569
bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4570
g.NavActivateId = backup_activate_id;
4571
PopStyleVar(3);
4572
PopStyleColor();
4573
if (!child_visible)
4574
{
4575
EndChild();
4576
EndGroup();
4577
return false;
4578
}
4579
draw_window = g.CurrentWindow; // Child window
4580
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.
4581
draw_window->DC.CursorPos += style.FramePadding;
4582
inner_size.x -= draw_window->ScrollbarSizes.x;
4583
}
4584
else
4585
{
4586
// Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4587
ItemSize(total_bb, style.FramePadding.y);
4588
if (!(flags & ImGuiInputTextFlags_MergedItem))
4589
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4590
return false;
4591
}
4592
4593
// Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4594
bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4595
if (hovered)
4596
SetMouseCursor(ImGuiMouseCursor_TextInput);
4597
if (hovered && g.NavHighlightItemUnderNav)
4598
hovered = false;
4599
4600
// We are only allowed to access the state if we are already the active widget.
4601
ImGuiInputTextState* state = GetInputTextState(id);
4602
4603
if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4604
flags |= ImGuiInputTextFlags_ReadOnly;
4605
const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4606
const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4607
const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4608
const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4609
if (is_resizable)
4610
IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4611
4612
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard || g.NavInputSource == ImGuiInputSource_Gamepad)));
4613
4614
const bool user_clicked = hovered && io.MouseClicked[0];
4615
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4616
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4617
bool clear_active_id = false;
4618
bool select_all = false;
4619
4620
float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4621
4622
const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4623
const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4624
const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4625
const bool init_state = (init_make_active || user_scroll_active);
4626
if (init_reload_from_user_buf)
4627
{
4628
int new_len = (int)ImStrlen(buf);
4629
IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4630
state->WantReloadUserBuf = false;
4631
InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len);
4632
state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4633
state->TextLen = new_len;
4634
memcpy(state->TextA.Data, buf, state->TextLen + 1);
4635
state->Stb->select_start = state->ReloadSelectionStart;
4636
state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
4637
state->CursorClamp();
4638
}
4639
else if ((init_state && g.ActiveId != id) || init_changed_specs)
4640
{
4641
// Access state even if we don't own it yet.
4642
state = &g.InputTextState;
4643
state->CursorAnimReset();
4644
4645
// 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)
4646
InputTextDeactivateHook(state->ID);
4647
4648
// Take a copy of the initial buffer value.
4649
// From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4650
const int buf_len = (int)ImStrlen(buf);
4651
IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4652
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.
4653
memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4654
4655
// Preserve cursor position and undo/redo stack if we come back to same widget
4656
// FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4657
bool recycle_state = (state->ID == id && !init_changed_specs);
4658
if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
4659
recycle_state = false;
4660
4661
// Start edition
4662
state->ID = id;
4663
state->TextLen = buf_len;
4664
if (!is_readonly)
4665
{
4666
state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4667
memcpy(state->TextA.Data, buf, state->TextLen + 1);
4668
}
4669
4670
// Find initial scroll position for right alignment
4671
state->Scroll = ImVec2(0.0f, 0.0f);
4672
if (flags & ImGuiInputTextFlags_ElideLeft)
4673
state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4674
4675
// Recycle existing cursor/selection/undo stack but clamp position
4676
// Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4677
if (recycle_state)
4678
state->CursorClamp();
4679
else
4680
stb_textedit_initialize_state(state->Stb, !is_multiline);
4681
4682
if (!is_multiline)
4683
{
4684
if (flags & ImGuiInputTextFlags_AutoSelectAll)
4685
select_all = true;
4686
if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4687
select_all = true;
4688
if (user_clicked && io.KeyCtrl)
4689
select_all = true;
4690
}
4691
4692
if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4693
state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4694
}
4695
4696
const bool is_osx = io.ConfigMacOSXBehaviors;
4697
if (g.ActiveId != id && init_make_active)
4698
{
4699
IM_ASSERT(state && state->ID == id);
4700
SetActiveID(id, window);
4701
SetFocusID(id, window);
4702
FocusWindow(window);
4703
}
4704
if (g.ActiveId == id)
4705
{
4706
// Declare some inputs, the other are registered and polled via Shortcut() routing system.
4707
// 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.
4708
const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4709
for (ImGuiKey key : always_owned_keys)
4710
SetKeyOwner(key, id);
4711
if (user_clicked)
4712
SetKeyOwner(ImGuiKey_MouseLeft, id);
4713
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4714
if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4715
{
4716
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4717
SetKeyOwner(ImGuiKey_UpArrow, id);
4718
SetKeyOwner(ImGuiKey_DownArrow, id);
4719
}
4720
if (is_multiline)
4721
{
4722
SetKeyOwner(ImGuiKey_PageUp, id);
4723
SetKeyOwner(ImGuiKey_PageDown, id);
4724
}
4725
// FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4726
if (is_osx)
4727
SetKeyOwner(ImGuiMod_Alt, id);
4728
4729
// Expose scroll in a manner that is agnostic to us using a child window
4730
if (is_multiline && state != NULL)
4731
state->Scroll.y = draw_window->Scroll.y;
4732
4733
// Read-only mode always ever read from source buffer. Refresh TextLen when active.
4734
if (is_readonly && state != NULL)
4735
state->TextLen = (int)ImStrlen(buf);
4736
//if (is_readonly && state != NULL)
4737
// state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4738
}
4739
if (state != NULL)
4740
state->TextSrc = is_readonly ? buf : state->TextA.Data;
4741
4742
// 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)
4743
if (g.ActiveId == id && state == NULL)
4744
ClearActiveID();
4745
4746
// Release focus when we click outside
4747
if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4748
clear_active_id = true;
4749
4750
// Lock the decision of whether we are going to take the path displaying the cursor or selection
4751
bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4752
bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4753
bool value_changed = false;
4754
bool validated = false;
4755
4756
// Select the buffer to render.
4757
const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4758
bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4759
4760
// Password pushes a temporary font with only a fallback glyph
4761
if (is_password && !is_displaying_hint)
4762
PushPasswordFont();
4763
4764
// Process mouse inputs and character inputs
4765
if (g.ActiveId == id)
4766
{
4767
IM_ASSERT(state != NULL);
4768
state->Edited = false;
4769
state->BufCapacity = buf_size;
4770
state->Flags = flags;
4771
4772
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4773
// Down the line we should have a cleaner library-wide concept of Selected vs Active.
4774
g.ActiveIdAllowOverlap = !io.MouseDown[0];
4775
4776
// Edit in progress
4777
const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4778
const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4779
4780
if (select_all)
4781
{
4782
state->SelectAll();
4783
state->SelectedAllMouseLock = true;
4784
}
4785
else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4786
{
4787
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4788
const int multiclick_count = (io.MouseClickedCount[0] - 2);
4789
if ((multiclick_count % 2) == 0)
4790
{
4791
// Double-click: Select word
4792
// We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4793
// FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4794
const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
4795
if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4796
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4797
//state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4798
if (!STB_TEXT_HAS_SELECTION(state->Stb))
4799
ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
4800
state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
4801
state->Stb->select_end = state->Stb->cursor;
4802
ImStb::stb_textedit_clamp(state, state->Stb);
4803
}
4804
else
4805
{
4806
// Triple-click: Select line
4807
const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
4808
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4809
state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4810
state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4811
if (!is_eol && is_multiline)
4812
{
4813
ImSwap(state->Stb->select_start, state->Stb->select_end);
4814
state->Stb->cursor = state->Stb->select_end;
4815
}
4816
state->CursorFollow = false;
4817
}
4818
state->CursorAnimReset();
4819
}
4820
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4821
{
4822
if (hovered)
4823
{
4824
if (io.KeyShift)
4825
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4826
else
4827
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4828
state->CursorAnimReset();
4829
}
4830
}
4831
else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4832
{
4833
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4834
state->CursorAnimReset();
4835
state->CursorFollow = true;
4836
}
4837
if (state->SelectedAllMouseLock && !io.MouseDown[0])
4838
state->SelectedAllMouseLock = false;
4839
4840
// We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4841
// (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)
4842
if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4843
{
4844
if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
4845
{
4846
unsigned int c = '\t'; // Insert TAB
4847
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4848
state->OnCharPressed(c);
4849
}
4850
// FIXME: Implement Shift+Tab
4851
/*
4852
if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4853
{
4854
}
4855
*/
4856
}
4857
4858
// Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4859
// 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.
4860
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4861
if (io.InputQueueCharacters.Size > 0)
4862
{
4863
if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4864
for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4865
{
4866
// Insert character if they pass filtering
4867
unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4868
if (c == '\t') // Skip Tab, see above.
4869
continue;
4870
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4871
state->OnCharPressed(c);
4872
}
4873
4874
// Consume characters
4875
io.InputQueueCharacters.resize(0);
4876
}
4877
}
4878
4879
// Process other shortcuts/key-presses
4880
bool revert_edit = false;
4881
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4882
{
4883
IM_ASSERT(state != NULL);
4884
4885
const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
4886
state->Stb->row_count_per_page = row_count_per_page;
4887
4888
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4889
const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4890
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
4891
4892
// 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)
4893
// Otherwise we could simply assume that we own the keys as we are active.
4894
const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4895
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());
4896
const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
4897
const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
4898
const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
4899
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;
4900
const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
4901
4902
// 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.
4903
const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4904
const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true);
4905
const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
4906
const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
4907
4908
// FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4909
// FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4910
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); }
4911
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); }
4912
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); }
4913
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); }
4914
else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4915
else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4916
else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4917
else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4918
else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
4919
{
4920
if (!state->HasSelection())
4921
{
4922
// 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)
4923
if (is_wordmove_key_down)
4924
state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4925
}
4926
state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4927
}
4928
else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
4929
{
4930
if (!state->HasSelection())
4931
{
4932
if (is_wordmove_key_down)
4933
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4934
else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4935
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4936
}
4937
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4938
}
4939
else if (is_enter_pressed || is_gamepad_validate)
4940
{
4941
// Determine if we turn Enter into a \n character
4942
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4943
if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4944
{
4945
validated = true;
4946
if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4947
state->SelectAll(); // No need to scroll
4948
else
4949
clear_active_id = true;
4950
}
4951
else if (!is_readonly)
4952
{
4953
unsigned int c = '\n'; // Insert new line
4954
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4955
state->OnCharPressed(c);
4956
}
4957
}
4958
else if (is_cancel)
4959
{
4960
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4961
{
4962
if (buf[0] != 0)
4963
{
4964
revert_edit = true;
4965
}
4966
else
4967
{
4968
render_cursor = render_selection = false;
4969
clear_active_id = true;
4970
}
4971
}
4972
else
4973
{
4974
clear_active_id = revert_edit = true;
4975
render_cursor = render_selection = false;
4976
}
4977
}
4978
else if (is_undo || is_redo)
4979
{
4980
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4981
state->ClearSelection();
4982
}
4983
else if (is_select_all)
4984
{
4985
state->SelectAll();
4986
state->CursorFollow = true;
4987
}
4988
else if (is_cut || is_copy)
4989
{
4990
// Cut, Copy
4991
if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
4992
{
4993
// SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
4994
const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
4995
const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
4996
g.TempBuffer.reserve(ie - ib + 1);
4997
memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib);
4998
g.TempBuffer.Data[ie - ib] = 0;
4999
SetClipboardText(g.TempBuffer.Data);
5000
}
5001
if (is_cut)
5002
{
5003
if (!state->HasSelection())
5004
state->SelectAll();
5005
state->CursorFollow = true;
5006
stb_textedit_cut(state, state->Stb);
5007
}
5008
}
5009
else if (is_paste)
5010
{
5011
if (const char* clipboard = GetClipboardText())
5012
{
5013
// Filter pasted buffer
5014
const int clipboard_len = (int)ImStrlen(clipboard);
5015
ImVector<char> clipboard_filtered;
5016
clipboard_filtered.reserve(clipboard_len + 1);
5017
for (const char* s = clipboard; *s != 0; )
5018
{
5019
unsigned int c;
5020
int in_len = ImTextCharFromUtf8(&c, s, NULL);
5021
s += in_len;
5022
if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true))
5023
continue;
5024
char c_utf8[5];
5025
ImTextCharToUtf8(c_utf8, c);
5026
int out_len = (int)ImStrlen(c_utf8);
5027
clipboard_filtered.resize(clipboard_filtered.Size + out_len);
5028
memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len);
5029
}
5030
if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
5031
{
5032
clipboard_filtered.push_back(0);
5033
stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1);
5034
state->CursorFollow = true;
5035
}
5036
}
5037
}
5038
5039
// Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
5040
render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
5041
}
5042
5043
// Process callbacks and apply result back to user's buffer.
5044
const char* apply_new_text = NULL;
5045
int apply_new_text_length = 0;
5046
if (g.ActiveId == id)
5047
{
5048
IM_ASSERT(state != NULL);
5049
if (revert_edit && !is_readonly)
5050
{
5051
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5052
{
5053
// Clear input
5054
IM_ASSERT(buf[0] != 0);
5055
apply_new_text = "";
5056
apply_new_text_length = 0;
5057
value_changed = true;
5058
IMSTB_TEXTEDIT_CHARTYPE empty_string;
5059
stb_textedit_replace(state, state->Stb, &empty_string, 0);
5060
}
5061
else if (strcmp(buf, state->TextToRevertTo.Data) != 0)
5062
{
5063
apply_new_text = state->TextToRevertTo.Data;
5064
apply_new_text_length = state->TextToRevertTo.Size - 1;
5065
5066
// Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5067
// Push records into the undo stack so we can CTRL+Z the revert operation itself
5068
value_changed = true;
5069
stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
5070
}
5071
}
5072
5073
// FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId,
5074
// even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks.
5075
// If we do that, need to ensure that as special case, 'validated == true' also writes back.
5076
// This also allows the user to use InputText() without maintaining any user-side storage.
5077
// (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
5078
// unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
5079
const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
5080
if (apply_edit_back_to_user_buffer)
5081
{
5082
// Apply current edited text immediately.
5083
// Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
5084
5085
// User callback
5086
if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5087
{
5088
IM_ASSERT(callback != NULL);
5089
5090
// The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5091
ImGuiInputTextFlags event_flag = 0;
5092
ImGuiKey event_key = ImGuiKey_None;
5093
if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
5094
{
5095
event_flag = ImGuiInputTextFlags_CallbackCompletion;
5096
event_key = ImGuiKey_Tab;
5097
}
5098
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
5099
{
5100
event_flag = ImGuiInputTextFlags_CallbackHistory;
5101
event_key = ImGuiKey_UpArrow;
5102
}
5103
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
5104
{
5105
event_flag = ImGuiInputTextFlags_CallbackHistory;
5106
event_key = ImGuiKey_DownArrow;
5107
}
5108
else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5109
{
5110
event_flag = ImGuiInputTextFlags_CallbackEdit;
5111
}
5112
else if (flags & ImGuiInputTextFlags_CallbackAlways)
5113
{
5114
event_flag = ImGuiInputTextFlags_CallbackAlways;
5115
}
5116
5117
if (event_flag)
5118
{
5119
ImGuiInputTextCallbackData callback_data;
5120
callback_data.Ctx = &g;
5121
callback_data.EventFlag = event_flag;
5122
callback_data.Flags = flags;
5123
callback_data.UserData = callback_user_data;
5124
5125
// FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5126
char* callback_buf = is_readonly ? buf : state->TextA.Data;
5127
IM_ASSERT(callback_buf == state->TextSrc);
5128
state->CallbackTextBackup.resize(state->TextLen + 1);
5129
memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);
5130
5131
callback_data.EventKey = event_key;
5132
callback_data.Buf = callback_buf;
5133
callback_data.BufTextLen = state->TextLen;
5134
callback_data.BufSize = state->BufCapacity;
5135
callback_data.BufDirty = false;
5136
5137
const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor;
5138
const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start;
5139
const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end;
5140
5141
// Call user code
5142
callback(&callback_data);
5143
5144
// Read back what user may have modified
5145
callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5146
IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5147
IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5148
IM_ASSERT(callback_data.Flags == flags);
5149
const bool buf_dirty = callback_data.BufDirty;
5150
if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; }
5151
if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; }
5152
if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; }
5153
if (buf_dirty)
5154
{
5155
// Callback may update buffer and thus set buf_dirty even in read-only mode.
5156
IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5157
InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen);
5158
state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5159
state->CursorAnimReset();
5160
}
5161
}
5162
}
5163
5164
// Will copy result string if modified
5165
if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
5166
{
5167
apply_new_text = state->TextSrc;
5168
apply_new_text_length = state->TextLen;
5169
value_changed = true;
5170
}
5171
}
5172
}
5173
5174
// Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5175
if (g.InputTextDeactivatedState.ID == id)
5176
{
5177
if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5178
{
5179
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5180
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5181
value_changed = true;
5182
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5183
}
5184
g.InputTextDeactivatedState.ID = 0;
5185
}
5186
5187
// Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5188
if (apply_new_text != NULL)
5189
{
5190
//// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
5191
//// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
5192
//// without any storage on user's side.
5193
IM_ASSERT(apply_new_text_length >= 0);
5194
if (is_resizable)
5195
{
5196
ImGuiInputTextCallbackData callback_data;
5197
callback_data.Ctx = &g;
5198
callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5199
callback_data.Flags = flags;
5200
callback_data.Buf = buf;
5201
callback_data.BufTextLen = apply_new_text_length;
5202
callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5203
callback_data.UserData = callback_user_data;
5204
callback(&callback_data);
5205
buf = callback_data.Buf;
5206
buf_size = callback_data.BufSize;
5207
apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5208
IM_ASSERT(apply_new_text_length <= buf_size);
5209
}
5210
//IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5211
5212
// If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5213
ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5214
}
5215
5216
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5217
// Otherwise request text input ahead for next frame.
5218
if (g.ActiveId == id && clear_active_id)
5219
ClearActiveID();
5220
5221
// Render frame
5222
if (!is_multiline)
5223
{
5224
RenderNavCursor(frame_bb, id);
5225
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5226
}
5227
5228
const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
5229
ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5230
ImVec2 text_size(0.0f, 0.0f);
5231
5232
// Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5233
// without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5234
// Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5235
const int buf_display_max_length = 2 * 1024 * 1024;
5236
const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5237
const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5238
5239
// Display hint when contents is empty
5240
// At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5241
const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5242
if (new_is_displaying_hint != is_displaying_hint)
5243
{
5244
if (is_password && !is_displaying_hint)
5245
PopPasswordFont();
5246
is_displaying_hint = new_is_displaying_hint;
5247
if (is_password && !is_displaying_hint)
5248
PushPasswordFont();
5249
}
5250
if (is_displaying_hint)
5251
{
5252
buf_display = hint;
5253
buf_display_end = hint + ImStrlen(hint);
5254
}
5255
5256
// Render text. We currently only render selection when the widget is active or while scrolling.
5257
// FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5258
if (render_cursor || render_selection)
5259
{
5260
IM_ASSERT(state != NULL);
5261
if (!is_displaying_hint)
5262
buf_display_end = buf_display + state->TextLen;
5263
5264
// Render text (with cursor and selection)
5265
// This is going to be messy. We need to:
5266
// - Display the text (this alone can be more easily clipped)
5267
// - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5268
// - Measure text height (for scrollbar)
5269
// 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)
5270
// FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5271
const char* text_begin = buf_display;
5272
const char* text_end = text_begin + state->TextLen;
5273
ImVec2 cursor_offset, select_start_offset;
5274
5275
{
5276
// Find lines numbers straddling cursor and selection min position
5277
int cursor_line_no = render_cursor ? -1 : -1000;
5278
int selmin_line_no = render_selection ? -1 : -1000;
5279
const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
5280
const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
5281
5282
// Count lines and find line number for cursor and selection ends
5283
int line_count = 1;
5284
if (is_multiline)
5285
{
5286
for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
5287
{
5288
if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
5289
if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
5290
line_count++;
5291
}
5292
}
5293
if (cursor_line_no == -1)
5294
cursor_line_no = line_count;
5295
if (selmin_line_no == -1)
5296
selmin_line_no = line_count;
5297
5298
// Calculate 2d position by finding the beginning of the line and measuring distance
5299
cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x;
5300
cursor_offset.y = cursor_line_no * g.FontSize;
5301
if (selmin_line_no >= 0)
5302
{
5303
select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x;
5304
select_start_offset.y = selmin_line_no * g.FontSize;
5305
}
5306
5307
// Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5308
if (is_multiline)
5309
text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5310
}
5311
5312
// Scroll
5313
if (render_cursor && state->CursorFollow)
5314
{
5315
// Horizontal scroll in chunks of quarter width
5316
if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5317
{
5318
const float scroll_increment_x = inner_size.x * 0.25f;
5319
const float visible_width = inner_size.x - style.FramePadding.x;
5320
if (cursor_offset.x < state->Scroll.x)
5321
state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5322
else if (cursor_offset.x - visible_width >= state->Scroll.x)
5323
state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5324
}
5325
else
5326
{
5327
state->Scroll.y = 0.0f;
5328
}
5329
5330
// Vertical scroll
5331
if (is_multiline)
5332
{
5333
// Test if cursor is vertically visible
5334
if (cursor_offset.y - g.FontSize < scroll_y)
5335
scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5336
else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5337
scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5338
const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5339
scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
5340
draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5341
draw_window->Scroll.y = scroll_y;
5342
}
5343
5344
state->CursorFollow = false;
5345
}
5346
5347
// Draw selection
5348
const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
5349
if (render_selection)
5350
{
5351
const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
5352
const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
5353
5354
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.
5355
float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection.
5356
float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5357
ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5358
for (const char* p = text_selected_begin; p < text_selected_end; )
5359
{
5360
if (rect_pos.y > clip_rect.w + g.FontSize)
5361
break;
5362
if (rect_pos.y < clip_rect.y)
5363
{
5364
p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p);
5365
p = p ? p + 1 : text_selected_end;
5366
}
5367
else
5368
{
5369
ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true);
5370
if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5371
ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5372
rect.ClipWith(clip_rect);
5373
if (rect.Overlaps(clip_rect))
5374
draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5375
rect_pos.x = draw_pos.x - draw_scroll.x;
5376
}
5377
rect_pos.y += g.FontSize;
5378
}
5379
}
5380
5381
// We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
5382
// FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
5383
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5384
{
5385
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5386
draw_window->DrawList->AddText(g.Font, g.FontSize, g.FontWeight, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5387
}
5388
5389
// Draw blinking cursor
5390
if (render_cursor)
5391
{
5392
state->CursorAnim += io.DeltaTime;
5393
bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5394
ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5395
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);
5396
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5397
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f); // FIXME-DPI: Cursor thickness (#7031)
5398
5399
// 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.)
5400
// This is required for some backends (SDL3) to start emitting character/text inputs.
5401
// As per #6341, make sure we don't set that on the deactivating frame.
5402
if (!is_readonly && g.ActiveId == id)
5403
{
5404
ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
5405
ime_data->WantVisible = true;
5406
ime_data->WantTextInput = true;
5407
ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5408
ime_data->InputLineHeight = g.FontSize;
5409
ime_data->ViewportId = window->Viewport->ID;
5410
}
5411
}
5412
}
5413
else
5414
{
5415
// Render text only (no selection, no cursor)
5416
if (is_multiline)
5417
text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width
5418
else if (!is_displaying_hint && g.ActiveId == id)
5419
buf_display_end = buf_display + state->TextLen;
5420
else if (!is_displaying_hint)
5421
buf_display_end = buf_display + ImStrlen(buf_display);
5422
5423
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5424
{
5425
// Find render position for right alignment
5426
if (flags & ImGuiInputTextFlags_ElideLeft)
5427
draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5428
5429
const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
5430
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5431
draw_window->DrawList->AddText(g.Font, g.FontSize, g.FontWeight, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5432
}
5433
}
5434
5435
if (is_password && !is_displaying_hint)
5436
PopPasswordFont();
5437
5438
if (is_multiline)
5439
{
5440
// For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5441
Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5442
g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5443
EndChild();
5444
item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5445
5446
// ...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...
5447
// FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5448
EndGroup();
5449
if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5450
{
5451
g.LastItemData.ID = id;
5452
g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5453
g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5454
}
5455
}
5456
if (state)
5457
state->TextSrc = NULL;
5458
5459
// Log as text
5460
if (g.LogEnabled && (!is_password || is_displaying_hint))
5461
{
5462
LogSetNextTextDecoration("{", "}");
5463
LogRenderedText(&draw_pos, buf_display, buf_display_end);
5464
}
5465
5466
if (label_size.x > 0)
5467
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
5468
5469
if (value_changed)
5470
MarkItemEdited(id);
5471
5472
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5473
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5474
return validated;
5475
else
5476
return value_changed;
5477
}
5478
5479
void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5480
{
5481
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5482
ImGuiContext& g = *GImGui;
5483
ImStb::STB_TexteditState* stb_state = state->Stb;
5484
ImStb::StbUndoState* undo_state = &stb_state->undostate;
5485
Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5486
DebugLocateItemOnHover(state->ID);
5487
Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5488
Text("BufCapacity: %d", state->BufCapacity);
5489
Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5490
Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5491
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);
5492
if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5493
{
5494
PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5495
for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5496
{
5497
ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5498
const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5499
if (undo_rec_type == ' ')
5500
BeginDisabled();
5501
const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5502
const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5503
Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5504
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);
5505
if (undo_rec_type == ' ')
5506
EndDisabled();
5507
}
5508
PopStyleVar();
5509
}
5510
EndChild();
5511
#else
5512
IM_UNUSED(state);
5513
#endif
5514
}
5515
5516
//-------------------------------------------------------------------------
5517
// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5518
//-------------------------------------------------------------------------
5519
// - ColorEdit3()
5520
// - ColorEdit4()
5521
// - ColorPicker3()
5522
// - RenderColorRectWithAlphaCheckerboard() [Internal]
5523
// - ColorPicker4()
5524
// - ColorButton()
5525
// - SetColorEditOptions()
5526
// - ColorTooltip() [Internal]
5527
// - ColorEditOptionsPopup() [Internal]
5528
// - ColorPickerOptionsPopup() [Internal]
5529
//-------------------------------------------------------------------------
5530
5531
bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5532
{
5533
return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5534
}
5535
5536
static void ColorEditRestoreH(const float* col, float* H)
5537
{
5538
ImGuiContext& g = *GImGui;
5539
IM_ASSERT(g.ColorEditCurrentID != 0);
5540
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5541
return;
5542
*H = g.ColorEditSavedHue;
5543
}
5544
5545
// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5546
// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5547
static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5548
{
5549
ImGuiContext& g = *GImGui;
5550
IM_ASSERT(g.ColorEditCurrentID != 0);
5551
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5552
return;
5553
5554
// When S == 0, H is undefined.
5555
// When H == 1 it wraps around to 0.
5556
if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5557
*H = g.ColorEditSavedHue;
5558
5559
// When V == 0, S is undefined.
5560
if (*V == 0.0f)
5561
*S = g.ColorEditSavedSat;
5562
}
5563
5564
// Edit colors components (each component in 0.0f..1.0f range).
5565
// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5566
// 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.
5567
bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5568
{
5569
ImGuiWindow* window = GetCurrentWindow();
5570
if (window->SkipItems)
5571
return false;
5572
5573
ImGuiContext& g = *GImGui;
5574
const ImGuiStyle& style = g.Style;
5575
const float square_sz = GetFrameHeight();
5576
const char* label_display_end = FindRenderedTextEnd(label);
5577
float w_full = CalcItemWidth();
5578
g.NextItemData.ClearFlags();
5579
5580
BeginGroup();
5581
PushID(label);
5582
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5583
if (set_current_color_edit_id)
5584
g.ColorEditCurrentID = window->IDStack.back();
5585
5586
// If we're not showing any slider there's no point in doing any HSV conversions
5587
const ImGuiColorEditFlags flags_untouched = flags;
5588
if (flags & ImGuiColorEditFlags_NoInputs)
5589
flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5590
5591
// Context menu: display and modify options (before defaults are applied)
5592
if (!(flags & ImGuiColorEditFlags_NoOptions))
5593
ColorEditOptionsPopup(col, flags);
5594
5595
// Read stored options
5596
if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5597
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5598
if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5599
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5600
if (!(flags & ImGuiColorEditFlags_PickerMask_))
5601
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5602
if (!(flags & ImGuiColorEditFlags_InputMask_))
5603
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5604
flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5605
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5606
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5607
5608
const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5609
const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5610
const int components = alpha ? 4 : 3;
5611
const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5612
const float w_inputs = ImMax(w_full - w_button, 1.0f);
5613
w_full = w_inputs + w_button;
5614
5615
// Convert to the formats we need
5616
float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5617
if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5618
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5619
else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5620
{
5621
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5622
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5623
ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5624
}
5625
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]) };
5626
5627
bool value_changed = false;
5628
bool value_changed_as_float = false;
5629
5630
const ImVec2 pos = window->DC.CursorPos;
5631
const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5632
window->DC.CursorPos.x = pos.x + inputs_offset_x;
5633
5634
if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5635
{
5636
// RGB/HSV 0..255 Sliders
5637
const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5638
5639
const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5640
static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5641
static const char* fmt_table_int[3][4] =
5642
{
5643
{ "%3d", "%3d", "%3d", "%3d" }, // Short display
5644
{ "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5645
{ "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5646
};
5647
static const char* fmt_table_float[3][4] =
5648
{
5649
{ "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5650
{ "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5651
{ "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5652
};
5653
const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5654
5655
float prev_split = 0.0f;
5656
for (int n = 0; n < components; n++)
5657
{
5658
if (n > 0)
5659
SameLine(0, style.ItemInnerSpacing.x);
5660
float next_split = IM_TRUNC(w_items * (n + 1) / components);
5661
SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5662
prev_split = next_split;
5663
5664
// FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5665
if (flags & ImGuiColorEditFlags_Float)
5666
{
5667
value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
5668
value_changed_as_float |= value_changed;
5669
}
5670
else
5671
{
5672
value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]);
5673
}
5674
if (!(flags & ImGuiColorEditFlags_NoOptions))
5675
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5676
}
5677
}
5678
else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5679
{
5680
// RGB Hexadecimal Input
5681
char buf[64];
5682
if (alpha)
5683
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255));
5684
else
5685
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5686
SetNextItemWidth(w_inputs);
5687
if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase))
5688
{
5689
value_changed = true;
5690
char* p = buf;
5691
while (*p == '#' || ImCharIsBlankA(*p))
5692
p++;
5693
i[0] = i[1] = i[2] = 0;
5694
i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5695
int r;
5696
if (alpha)
5697
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)
5698
else
5699
r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5700
IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5701
}
5702
if (!(flags & ImGuiColorEditFlags_NoOptions))
5703
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5704
}
5705
5706
ImGuiWindow* picker_active_window = NULL;
5707
if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5708
{
5709
const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5710
window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5711
5712
const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5713
if (ColorButton("##ColorButton", col_v4, flags))
5714
{
5715
if (!(flags & ImGuiColorEditFlags_NoPicker))
5716
{
5717
// Store current color and open a picker
5718
g.ColorPickerRef = col_v4;
5719
OpenPopup("picker");
5720
SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5721
}
5722
}
5723
if (!(flags & ImGuiColorEditFlags_NoOptions))
5724
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5725
5726
if (BeginPopup("picker"))
5727
{
5728
if (g.CurrentWindow->BeginCount == 1)
5729
{
5730
picker_active_window = g.CurrentWindow;
5731
if (label != label_display_end)
5732
{
5733
TextEx(label, label_display_end);
5734
Spacing();
5735
}
5736
ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5737
ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5738
SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5739
value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
5740
}
5741
EndPopup();
5742
}
5743
}
5744
5745
if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5746
{
5747
// Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5748
// but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5749
SameLine(0.0f, style.ItemInnerSpacing.x);
5750
window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5751
TextEx(label, label_display_end);
5752
}
5753
5754
// Convert back
5755
if (value_changed && picker_active_window == NULL)
5756
{
5757
if (!value_changed_as_float)
5758
for (int n = 0; n < 4; n++)
5759
f[n] = i[n] / 255.0f;
5760
if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5761
{
5762
g.ColorEditSavedHue = f[0];
5763
g.ColorEditSavedSat = f[1];
5764
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5765
g.ColorEditSavedID = g.ColorEditCurrentID;
5766
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
5767
}
5768
if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5769
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5770
5771
col[0] = f[0];
5772
col[1] = f[1];
5773
col[2] = f[2];
5774
if (alpha)
5775
col[3] = f[3];
5776
}
5777
5778
if (set_current_color_edit_id)
5779
g.ColorEditCurrentID = 0;
5780
PopID();
5781
EndGroup();
5782
5783
// Drag and Drop Target
5784
// NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5785
if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5786
{
5787
bool accepted_drag_drop = false;
5788
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5789
{
5790
memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5791
value_changed = accepted_drag_drop = true;
5792
}
5793
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5794
{
5795
memcpy((float*)col, payload->Data, sizeof(float) * components);
5796
value_changed = accepted_drag_drop = true;
5797
}
5798
5799
// Drag-drop payloads are always RGB
5800
if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5801
ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
5802
EndDragDropTarget();
5803
}
5804
5805
// When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5806
if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5807
g.LastItemData.ID = g.ActiveId;
5808
5809
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5810
MarkItemEdited(g.LastItemData.ID);
5811
5812
return value_changed;
5813
}
5814
5815
bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5816
{
5817
float col4[4] = { col[0], col[1], col[2], 1.0f };
5818
if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
5819
return false;
5820
col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5821
return true;
5822
}
5823
5824
// Helper for ColorPicker4()
5825
static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5826
{
5827
ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5828
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));
5829
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5830
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));
5831
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
5832
}
5833
5834
// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5835
// (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.)
5836
// 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..)
5837
// 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)
5838
bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5839
{
5840
ImGuiContext& g = *GImGui;
5841
ImGuiWindow* window = GetCurrentWindow();
5842
if (window->SkipItems)
5843
return false;
5844
5845
ImDrawList* draw_list = window->DrawList;
5846
ImGuiStyle& style = g.Style;
5847
ImGuiIO& io = g.IO;
5848
5849
const float width = CalcItemWidth();
5850
const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5851
g.NextItemData.ClearFlags();
5852
5853
PushID(label);
5854
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5855
if (set_current_color_edit_id)
5856
g.ColorEditCurrentID = window->IDStack.back();
5857
BeginGroup();
5858
5859
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5860
flags |= ImGuiColorEditFlags_NoSmallPreview;
5861
5862
// Context menu: display and store options.
5863
if (!(flags & ImGuiColorEditFlags_NoOptions))
5864
ColorPickerOptionsPopup(col, flags);
5865
5866
// Read stored options
5867
if (!(flags & ImGuiColorEditFlags_PickerMask_))
5868
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5869
if (!(flags & ImGuiColorEditFlags_InputMask_))
5870
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5871
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5872
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5873
if (!(flags & ImGuiColorEditFlags_NoOptions))
5874
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5875
5876
// Setup
5877
int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5878
bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5879
ImVec2 picker_pos = window->DC.CursorPos;
5880
float square_sz = GetFrameHeight();
5881
float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5882
float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5883
float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5884
float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5885
float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5886
5887
float backup_initial_col[4];
5888
memcpy(backup_initial_col, col, components * sizeof(float));
5889
5890
float wheel_thickness = sv_picker_size * 0.08f;
5891
float wheel_r_outer = sv_picker_size * 0.50f;
5892
float wheel_r_inner = wheel_r_outer - wheel_thickness;
5893
ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5894
5895
// Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5896
float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5897
ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5898
ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5899
ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5900
5901
float H = col[0], S = col[1], V = col[2];
5902
float R = col[0], G = col[1], B = col[2];
5903
if (flags & ImGuiColorEditFlags_InputRGB)
5904
{
5905
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5906
ColorConvertRGBtoHSV(R, G, B, H, S, V);
5907
ColorEditRestoreHS(col, &H, &S, &V);
5908
}
5909
else if (flags & ImGuiColorEditFlags_InputHSV)
5910
{
5911
ColorConvertHSVtoRGB(H, S, V, R, G, B);
5912
}
5913
5914
bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5915
5916
PushItemFlag(ImGuiItemFlags_NoNav, true);
5917
if (flags & ImGuiColorEditFlags_PickerHueWheel)
5918
{
5919
// Hue wheel + SV triangle logic
5920
InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5921
if (IsItemActive() && !is_readonly)
5922
{
5923
ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5924
ImVec2 current_off = g.IO.MousePos - wheel_center;
5925
float initial_dist2 = ImLengthSqr(initial_off);
5926
if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5927
{
5928
// Interactive with Hue wheel
5929
H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5930
if (H < 0.0f)
5931
H += 1.0f;
5932
value_changed = value_changed_h = true;
5933
}
5934
float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5935
float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5936
if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
5937
{
5938
// Interacting with SV triangle
5939
ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
5940
if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
5941
current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
5942
float uu, vv, ww;
5943
ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
5944
V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
5945
S = ImClamp(uu / V, 0.0001f, 1.0f);
5946
value_changed = value_changed_sv = true;
5947
}
5948
}
5949
if (!(flags & ImGuiColorEditFlags_NoOptions))
5950
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5951
}
5952
else if (flags & ImGuiColorEditFlags_PickerHueBar)
5953
{
5954
// SV rectangle logic
5955
InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
5956
if (IsItemActive() && !is_readonly)
5957
{
5958
S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5959
V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5960
ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5961
value_changed = value_changed_sv = true;
5962
}
5963
if (!(flags & ImGuiColorEditFlags_NoOptions))
5964
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5965
5966
// Hue bar logic
5967
SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5968
InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
5969
if (IsItemActive() && !is_readonly)
5970
{
5971
H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5972
value_changed = value_changed_h = true;
5973
}
5974
}
5975
5976
// Alpha bar logic
5977
if (alpha_bar)
5978
{
5979
SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5980
InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
5981
if (IsItemActive())
5982
{
5983
col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5984
value_changed = true;
5985
}
5986
}
5987
PopItemFlag(); // ImGuiItemFlags_NoNav
5988
5989
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5990
{
5991
SameLine(0, style.ItemInnerSpacing.x);
5992
BeginGroup();
5993
}
5994
5995
if (!(flags & ImGuiColorEditFlags_NoLabel))
5996
{
5997
const char* label_display_end = FindRenderedTextEnd(label);
5998
if (label != label_display_end)
5999
{
6000
if ((flags & ImGuiColorEditFlags_NoSidePreview))
6001
SameLine(0, style.ItemInnerSpacing.x);
6002
TextEx(label, label_display_end);
6003
}
6004
}
6005
6006
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6007
{
6008
PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
6009
ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6010
if ((flags & ImGuiColorEditFlags_NoLabel))
6011
Text("Current");
6012
6013
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
6014
ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
6015
if (ref_col != NULL)
6016
{
6017
Text("Original");
6018
ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
6019
if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
6020
{
6021
memcpy(col, ref_col, components * sizeof(float));
6022
value_changed = true;
6023
}
6024
}
6025
PopItemFlag();
6026
EndGroup();
6027
}
6028
6029
// Convert back color to RGB
6030
if (value_changed_h || value_changed_sv)
6031
{
6032
if (flags & ImGuiColorEditFlags_InputRGB)
6033
{
6034
ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
6035
g.ColorEditSavedHue = H;
6036
g.ColorEditSavedSat = S;
6037
g.ColorEditSavedID = g.ColorEditCurrentID;
6038
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
6039
}
6040
else if (flags & ImGuiColorEditFlags_InputHSV)
6041
{
6042
col[0] = H;
6043
col[1] = S;
6044
col[2] = V;
6045
}
6046
}
6047
6048
// R,G,B and H,S,V slider color editor
6049
bool value_changed_fix_hue_wrap = false;
6050
if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
6051
{
6052
PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6053
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6054
ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6055
if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6056
if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
6057
{
6058
// FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6059
// 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)
6060
value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6061
value_changed = true;
6062
}
6063
if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6064
value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
6065
if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6066
value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
6067
PopItemWidth();
6068
}
6069
6070
// Try to cancel hue wrap (after ColorEdit4 call), if any
6071
if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6072
{
6073
float new_H, new_S, new_V;
6074
ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
6075
if (new_H <= 0 && H > 0)
6076
{
6077
if (new_V <= 0 && V != new_V)
6078
ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
6079
else if (new_S <= 0)
6080
ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
6081
}
6082
}
6083
6084
if (value_changed)
6085
{
6086
if (flags & ImGuiColorEditFlags_InputRGB)
6087
{
6088
R = col[0];
6089
G = col[1];
6090
B = col[2];
6091
ColorConvertRGBtoHSV(R, G, B, H, S, V);
6092
ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
6093
}
6094
else if (flags & ImGuiColorEditFlags_InputHSV)
6095
{
6096
H = col[0];
6097
S = col[1];
6098
V = col[2];
6099
ColorConvertHSVtoRGB(H, S, V, R, G, B);
6100
}
6101
}
6102
6103
const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6104
const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6105
const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6106
const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6107
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) };
6108
6109
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);
6110
ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
6111
ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6112
6113
ImVec2 sv_cursor_pos;
6114
6115
if (flags & ImGuiColorEditFlags_PickerHueWheel)
6116
{
6117
// Render Hue Wheel
6118
const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6119
const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
6120
for (int n = 0; n < 6; n++)
6121
{
6122
const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6123
const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6124
const int vert_start_idx = draw_list->VtxBuffer.Size;
6125
draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6126
draw_list->PathStroke(col_white, 0, wheel_thickness);
6127
const int vert_end_idx = draw_list->VtxBuffer.Size;
6128
6129
// Paint colors over existing vertices
6130
ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6131
ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6132
ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6133
}
6134
6135
// Render Cursor + preview on Hue Wheel
6136
float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6137
float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6138
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);
6139
float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6140
int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6141
draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6142
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6143
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6144
6145
// Render SV triangle (rotated according to hue)
6146
ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6147
ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6148
ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6149
ImVec2 uv_white = GetFontTexUvWhitePixel();
6150
draw_list->PrimReserve(3, 3);
6151
draw_list->PrimVtx(tra, uv_white, hue_color32);
6152
draw_list->PrimVtx(trb, uv_white, col_black);
6153
draw_list->PrimVtx(trc, uv_white, col_white);
6154
draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6155
sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6156
}
6157
else if (flags & ImGuiColorEditFlags_PickerHueBar)
6158
{
6159
// Render SV Square
6160
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6161
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6162
RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6163
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
6164
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);
6165
6166
// Render Hue Bar
6167
for (int i = 0; i < 6; ++i)
6168
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]);
6169
float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6170
RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6171
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);
6172
}
6173
6174
// Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6175
float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6176
int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6177
draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6178
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6179
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6180
6181
// Render alpha bar
6182
if (alpha_bar)
6183
{
6184
float alpha = ImSaturate(col[3]);
6185
ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6186
RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6187
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);
6188
float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6189
RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6190
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);
6191
}
6192
6193
EndGroup();
6194
6195
if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6196
value_changed = false;
6197
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6198
MarkItemEdited(g.LastItemData.ID);
6199
6200
if (set_current_color_edit_id)
6201
g.ColorEditCurrentID = 0;
6202
PopID();
6203
6204
return value_changed;
6205
}
6206
6207
// A little color square. Return true when clicked.
6208
// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6209
// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6210
// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6211
bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6212
{
6213
ImGuiWindow* window = GetCurrentWindow();
6214
if (window->SkipItems)
6215
return false;
6216
6217
ImGuiContext& g = *GImGui;
6218
const ImGuiID id = window->GetID(desc_id);
6219
const float default_size = GetFrameHeight();
6220
const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6221
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6222
ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6223
if (!ItemAdd(bb, id))
6224
return false;
6225
6226
bool hovered, held;
6227
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6228
6229
if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6230
flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6231
6232
ImVec4 col_rgb = col;
6233
if (flags & ImGuiColorEditFlags_InputHSV)
6234
ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6235
6236
ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6237
float grid_step = ImMin(size.x, size.y) / 2.99f;
6238
float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6239
ImRect bb_inner = bb;
6240
float off = 0.0f;
6241
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6242
{
6243
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.
6244
bb_inner.Expand(off);
6245
}
6246
if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6247
{
6248
float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6249
if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6250
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);
6251
else
6252
window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight);
6253
window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6254
}
6255
else
6256
{
6257
// Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6258
ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6259
if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6260
RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6261
else
6262
window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6263
}
6264
RenderNavCursor(bb, id);
6265
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6266
{
6267
if (g.Style.FrameBorderSize > 0.0f)
6268
RenderFrameBorder(bb.Min, bb.Max, rounding);
6269
else
6270
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border // FIXME-DPI
6271
}
6272
6273
// Drag and Drop Source
6274
// NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6275
if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6276
{
6277
if (flags & ImGuiColorEditFlags_NoAlpha)
6278
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6279
else
6280
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6281
ColorButton(desc_id, col, flags);
6282
SameLine();
6283
TextEx("Color");
6284
EndDragDropSource();
6285
}
6286
6287
// Tooltip
6288
if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6289
ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6290
6291
return pressed;
6292
}
6293
6294
// Initialize/override default color options
6295
void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6296
{
6297
ImGuiContext& g = *GImGui;
6298
if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6299
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6300
if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6301
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6302
if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6303
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6304
if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6305
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6306
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6307
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6308
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6309
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6310
g.ColorEditOptions = flags;
6311
}
6312
6313
// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6314
void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6315
{
6316
ImGuiContext& g = *GImGui;
6317
6318
if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6319
return;
6320
const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6321
if (text_end > text)
6322
{
6323
TextEx(text, text_end);
6324
Separator();
6325
}
6326
6327
ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6328
ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6329
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]);
6330
ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6331
ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz);
6332
SameLine();
6333
if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6334
{
6335
if (flags & ImGuiColorEditFlags_NoAlpha)
6336
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]);
6337
else
6338
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]);
6339
}
6340
else if (flags & ImGuiColorEditFlags_InputHSV)
6341
{
6342
if (flags & ImGuiColorEditFlags_NoAlpha)
6343
Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6344
else
6345
Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6346
}
6347
EndTooltip();
6348
}
6349
6350
void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6351
{
6352
bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6353
bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6354
if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6355
return;
6356
6357
ImGuiContext& g = *GImGui;
6358
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6359
ImGuiColorEditFlags opts = g.ColorEditOptions;
6360
if (allow_opt_inputs)
6361
{
6362
if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6363
if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6364
if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6365
}
6366
if (allow_opt_datatype)
6367
{
6368
if (allow_opt_inputs) Separator();
6369
if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6370
if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6371
}
6372
6373
if (allow_opt_inputs || allow_opt_datatype)
6374
Separator();
6375
if (Button("Copy as..", ImVec2(-1, 0)))
6376
OpenPopup("Copy");
6377
if (BeginPopup("Copy"))
6378
{
6379
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]);
6380
char buf[64];
6381
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6382
if (Selectable(buf))
6383
SetClipboardText(buf);
6384
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6385
if (Selectable(buf))
6386
SetClipboardText(buf);
6387
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb);
6388
if (Selectable(buf))
6389
SetClipboardText(buf);
6390
if (!(flags & ImGuiColorEditFlags_NoAlpha))
6391
{
6392
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6393
if (Selectable(buf))
6394
SetClipboardText(buf);
6395
}
6396
EndPopup();
6397
}
6398
6399
g.ColorEditOptions = opts;
6400
PopItemFlag();
6401
EndPopup();
6402
}
6403
6404
void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6405
{
6406
bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6407
bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6408
if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6409
return;
6410
6411
ImGuiContext& g = *GImGui;
6412
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6413
if (allow_opt_picker)
6414
{
6415
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
6416
PushItemWidth(picker_size.x);
6417
for (int picker_type = 0; picker_type < 2; picker_type++)
6418
{
6419
// Draw small/thumbnail version of each picker type (over an invisible button for selection)
6420
if (picker_type > 0) Separator();
6421
PushID(picker_type);
6422
ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6423
if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6424
if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6425
ImVec2 backup_pos = GetCursorScreenPos();
6426
if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6427
g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6428
SetCursorScreenPos(backup_pos);
6429
ImVec4 previewing_ref_col;
6430
memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6431
ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6432
PopID();
6433
}
6434
PopItemWidth();
6435
}
6436
if (allow_opt_alpha_bar)
6437
{
6438
if (allow_opt_picker) Separator();
6439
CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
6440
}
6441
PopItemFlag();
6442
EndPopup();
6443
}
6444
6445
//-------------------------------------------------------------------------
6446
// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6447
//-------------------------------------------------------------------------
6448
// - TreeNode()
6449
// - TreeNodeV()
6450
// - TreeNodeEx()
6451
// - TreeNodeExV()
6452
// - TreeNodeStoreStackData() [Internal]
6453
// - TreeNodeBehavior() [Internal]
6454
// - TreePush()
6455
// - TreePop()
6456
// - GetTreeNodeToLabelSpacing()
6457
// - SetNextItemOpen()
6458
// - CollapsingHeader()
6459
//-------------------------------------------------------------------------
6460
6461
bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6462
{
6463
va_list args;
6464
va_start(args, fmt);
6465
bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6466
va_end(args);
6467
return is_open;
6468
}
6469
6470
bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6471
{
6472
va_list args;
6473
va_start(args, fmt);
6474
bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6475
va_end(args);
6476
return is_open;
6477
}
6478
6479
bool ImGui::TreeNode(const char* label)
6480
{
6481
ImGuiWindow* window = GetCurrentWindow();
6482
if (window->SkipItems)
6483
return false;
6484
ImGuiID id = window->GetID(label);
6485
return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6486
}
6487
6488
bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6489
{
6490
return TreeNodeExV(str_id, 0, fmt, args);
6491
}
6492
6493
bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6494
{
6495
return TreeNodeExV(ptr_id, 0, fmt, args);
6496
}
6497
6498
bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6499
{
6500
ImGuiWindow* window = GetCurrentWindow();
6501
if (window->SkipItems)
6502
return false;
6503
ImGuiID id = window->GetID(label);
6504
return TreeNodeBehavior(id, flags, label, NULL);
6505
}
6506
6507
bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6508
{
6509
va_list args;
6510
va_start(args, fmt);
6511
bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6512
va_end(args);
6513
return is_open;
6514
}
6515
6516
bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6517
{
6518
va_list args;
6519
va_start(args, fmt);
6520
bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6521
va_end(args);
6522
return is_open;
6523
}
6524
6525
bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6526
{
6527
ImGuiWindow* window = GetCurrentWindow();
6528
if (window->SkipItems)
6529
return false;
6530
6531
ImGuiID id = window->GetID(str_id);
6532
const char* label, *label_end;
6533
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6534
return TreeNodeBehavior(id, flags, label, label_end);
6535
}
6536
6537
bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6538
{
6539
ImGuiWindow* window = GetCurrentWindow();
6540
if (window->SkipItems)
6541
return false;
6542
6543
ImGuiID id = window->GetID(ptr_id);
6544
const char* label, *label_end;
6545
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6546
return TreeNodeBehavior(id, flags, label, label_end);
6547
}
6548
6549
bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6550
{
6551
ImGuiContext& g = *GImGui;
6552
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6553
return storage->GetInt(storage_id, 0) != 0;
6554
}
6555
6556
void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6557
{
6558
ImGuiContext& g = *GImGui;
6559
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6560
storage->SetInt(storage_id, open ? 1 : 0);
6561
}
6562
6563
bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6564
{
6565
if (flags & ImGuiTreeNodeFlags_Leaf)
6566
return true;
6567
6568
// We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6569
ImGuiContext& g = *GImGui;
6570
ImGuiWindow* window = g.CurrentWindow;
6571
ImGuiStorage* storage = window->DC.StateStorage;
6572
6573
bool is_open;
6574
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6575
{
6576
if (g.NextItemData.OpenCond & ImGuiCond_Always)
6577
{
6578
is_open = g.NextItemData.OpenVal;
6579
TreeNodeSetOpen(storage_id, is_open);
6580
}
6581
else
6582
{
6583
// We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6584
const int stored_value = storage->GetInt(storage_id, -1);
6585
if (stored_value == -1)
6586
{
6587
is_open = g.NextItemData.OpenVal;
6588
TreeNodeSetOpen(storage_id, is_open);
6589
}
6590
else
6591
{
6592
is_open = stored_value != 0;
6593
}
6594
}
6595
}
6596
else
6597
{
6598
is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6599
}
6600
6601
// When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6602
// NB- If we are above max depth we still allow manually opened nodes to be logged.
6603
if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6604
is_open = true;
6605
6606
return is_open;
6607
}
6608
6609
// Store ImGuiTreeNodeStackData for just submitted node.
6610
// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6611
static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
6612
{
6613
ImGuiContext& g = *GImGui;
6614
ImGuiWindow* window = g.CurrentWindow;
6615
6616
g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6617
ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6618
tree_node_data->ID = g.LastItemData.ID;
6619
tree_node_data->TreeFlags = flags;
6620
tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6621
tree_node_data->NavRect = g.LastItemData.NavRect;
6622
6623
// Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
6624
const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
6625
tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
6626
tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
6627
tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
6628
window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6629
if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
6630
window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
6631
}
6632
6633
// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop.
6634
bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6635
{
6636
ImGuiWindow* window = GetCurrentWindow();
6637
if (window->SkipItems)
6638
return false;
6639
6640
ImGuiContext& g = *GImGui;
6641
const ImGuiStyle& style = g.Style;
6642
const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6643
const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6644
6645
if (!label_end)
6646
label_end = FindRenderedTextEnd(label);
6647
const ImVec2 label_size = CalcTextSize(label, label_end, false);
6648
6649
const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6650
const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6651
const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6652
6653
// We vertically grow up to current line height up the typical widget height.
6654
const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2);
6655
const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6656
const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6657
ImRect frame_bb;
6658
frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6659
frame_bb.Min.y = window->DC.CursorPos.y;
6660
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;
6661
frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6662
if (display_frame)
6663
{
6664
const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6665
frame_bb.Min.x -= outer_extend;
6666
frame_bb.Max.x += outer_extend;
6667
}
6668
6669
ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6670
ItemSize(ImVec2(text_width, frame_height), padding.y);
6671
6672
// For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6673
ImRect interact_bb = frame_bb;
6674
if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6675
interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6676
6677
// Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6678
ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6679
bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6680
6681
bool is_visible;
6682
if (span_all_columns || span_all_columns_label)
6683
{
6684
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6685
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6686
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6687
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6688
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6689
is_visible = ItemAdd(interact_bb, id);
6690
window->ClipRect.Min.x = backup_clip_rect_min_x;
6691
window->ClipRect.Max.x = backup_clip_rect_max_x;
6692
}
6693
else
6694
{
6695
is_visible = ItemAdd(interact_bb, id);
6696
}
6697
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6698
g.LastItemData.DisplayRect = frame_bb;
6699
6700
// If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
6701
// Store data for the current depth to allow returning to this node from any child item.
6702
// For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6703
// It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
6704
bool store_tree_node_stack_data = false;
6705
if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
6706
flags |= g.Style.TreeLinesFlags;
6707
const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
6708
if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6709
{
6710
store_tree_node_stack_data = draw_tree_lines;
6711
if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
6712
if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6713
store_tree_node_stack_data = true;
6714
}
6715
6716
const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6717
if (!is_visible)
6718
{
6719
if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
6720
{
6721
ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6722
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.
6723
if (frame_bb.Min.y >= window->ClipRect.Max.y)
6724
window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
6725
}
6726
if (is_open && store_tree_node_stack_data)
6727
TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6728
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6729
TreePushOverrideID(id);
6730
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6731
return is_open;
6732
}
6733
6734
if (span_all_columns || span_all_columns_label)
6735
{
6736
TablePushBackgroundChannel();
6737
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6738
g.LastItemData.ClipRect = window->ClipRect;
6739
}
6740
6741
ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6742
if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6743
button_flags |= ImGuiButtonFlags_AllowOverlap;
6744
if (!is_leaf)
6745
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6746
6747
// We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6748
// allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6749
// When clicking on the rest of the tree node we always disallow keyboard modifiers.
6750
const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6751
const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6752
const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6753
6754
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6755
if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6756
flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6757
6758
// Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6759
// Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6760
// - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6761
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6762
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6763
// - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6764
// - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6765
// It is rather standard that arrow click react on Down rather than Up.
6766
// 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.
6767
if (is_mouse_x_over_arrow)
6768
button_flags |= ImGuiButtonFlags_PressedOnClick;
6769
else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6770
button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6771
else
6772
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6773
if (flags & ImGuiTreeNodeFlags_NoNavFocus)
6774
button_flags |= ImGuiButtonFlags_NoNavFocus;
6775
6776
bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6777
const bool was_selected = selected;
6778
6779
// Multi-selection support (header)
6780
if (is_multi_select)
6781
{
6782
// Handle multi-select + alter button flags for it
6783
MultiSelectItemHeader(id, &selected, &button_flags);
6784
if (is_mouse_x_over_arrow)
6785
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6786
}
6787
else
6788
{
6789
if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6790
button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6791
}
6792
6793
bool hovered, held;
6794
bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
6795
bool toggled = false;
6796
if (!is_leaf)
6797
{
6798
if (pressed && g.DragDropHoldJustPressedId != id)
6799
{
6800
if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6801
toggled = true; // Single click
6802
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6803
toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6804
if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6805
toggled = true; // Double click
6806
}
6807
else if (pressed && g.DragDropHoldJustPressedId == id)
6808
{
6809
IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6810
if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6811
toggled = true;
6812
else
6813
pressed = false; // Cancel press so it doesn't trigger selection.
6814
}
6815
6816
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6817
{
6818
toggled = true;
6819
NavClearPreferredPosForAxis(ImGuiAxis_X);
6820
NavMoveRequestCancel();
6821
}
6822
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?
6823
{
6824
toggled = true;
6825
NavClearPreferredPosForAxis(ImGuiAxis_X);
6826
NavMoveRequestCancel();
6827
}
6828
6829
if (toggled)
6830
{
6831
is_open = !is_open;
6832
window->DC.StateStorage->SetInt(storage_id, is_open);
6833
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6834
}
6835
}
6836
6837
// Multi-selection support (footer)
6838
if (is_multi_select)
6839
{
6840
bool pressed_copy = pressed && !toggled;
6841
MultiSelectItemFooter(id, &selected, &pressed_copy);
6842
if (pressed)
6843
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
6844
}
6845
6846
if (selected != was_selected)
6847
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6848
6849
// Render
6850
{
6851
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
6852
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
6853
if (is_multi_select)
6854
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
6855
if (display_frame)
6856
{
6857
// Framed type
6858
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6859
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
6860
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6861
if (span_all_columns && !span_all_columns_label)
6862
TablePopBackgroundChannel();
6863
if (flags & ImGuiTreeNodeFlags_Bullet)
6864
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
6865
else if (!is_leaf)
6866
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);
6867
else // Leaf without bullet, left-adjusted text
6868
text_pos.x -= text_offset_x - padding.x;
6869
if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6870
frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6871
if (g.LogEnabled)
6872
LogSetNextTextDecoration("###", "###");
6873
}
6874
else
6875
{
6876
// Unframed typed for tree nodes
6877
if (hovered || selected)
6878
{
6879
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6880
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
6881
}
6882
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6883
if (span_all_columns && !span_all_columns_label)
6884
TablePopBackgroundChannel();
6885
if (flags & ImGuiTreeNodeFlags_Bullet)
6886
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
6887
else if (!is_leaf)
6888
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);
6889
if (g.LogEnabled)
6890
LogSetNextTextDecoration(">", NULL);
6891
}
6892
6893
if (draw_tree_lines)
6894
TreeNodeDrawLineToChildNode(ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
6895
6896
// Label
6897
if (display_frame)
6898
RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
6899
else
6900
RenderText(text_pos, label, label_end, false);
6901
6902
if (span_all_columns_label)
6903
TablePopBackgroundChannel();
6904
}
6905
6906
if (is_open && store_tree_node_stack_data)
6907
TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6908
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6909
TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6910
6911
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6912
return is_open;
6913
}
6914
6915
// Draw horizontal line from our parent node
6916
// This is only called for visible child nodes so we are not too fussy anymore about performances
6917
void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
6918
{
6919
ImGuiContext& g = *GImGui;
6920
ImGuiWindow* window = g.CurrentWindow;
6921
if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
6922
return;
6923
6924
ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6925
float x1 = ImTrunc(parent_data->DrawLinesX1);
6926
float x2 = ImTrunc(target_pos.x - g.Style.ItemInnerSpacing.x);
6927
float y = ImTrunc(target_pos.y);
6928
float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(x2 - x1, g.Style.TreeLinesRounding) : 0.0f;
6929
parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, y - rounding);
6930
if (x1 >= x2)
6931
return;
6932
if (rounding > 0.0f)
6933
{
6934
x1 += 0.5f + rounding;
6935
window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
6936
if (x1 < x2)
6937
window->DrawList->PathLineTo(ImVec2(x2, y));
6938
window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), ImDrawFlags_None, g.Style.TreeLinesSize);
6939
}
6940
else
6941
{
6942
window->DrawList->AddLine(ImVec2(x1, y), ImVec2(x2, y), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
6943
}
6944
}
6945
6946
// Draw vertical line of the hierarchy
6947
void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
6948
{
6949
ImGuiContext& g = *GImGui;
6950
ImGuiWindow* window = g.CurrentWindow;
6951
float y1 = ImMax(data->NavRect.Max.y, window->ClipRect.Min.y);
6952
float y2 = data->DrawLinesToNodesY2;
6953
if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
6954
{
6955
float y2_full = window->DC.CursorPos.y;
6956
if (g.CurrentTable)
6957
y2_full = ImMax(g.CurrentTable->RowPosY2, y2_full);
6958
y2_full = ImTrunc(y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
6959
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
6960
y2 = y2_full;
6961
}
6962
y2 = ImMin(y2, window->ClipRect.Max.y);
6963
if (y2 <= y1)
6964
return;
6965
float x = ImTrunc(data->DrawLinesX1);
6966
if (data->DrawLinesTableColumn != -1)
6967
TablePushColumnChannel(data->DrawLinesTableColumn);
6968
window->DrawList->AddLine(ImVec2(x, y1), ImVec2(x, y2), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
6969
if (data->DrawLinesTableColumn != -1)
6970
TablePopColumnChannel();
6971
}
6972
6973
void ImGui::TreePush(const char* str_id)
6974
{
6975
ImGuiWindow* window = GetCurrentWindow();
6976
Indent();
6977
window->DC.TreeDepth++;
6978
PushID(str_id);
6979
}
6980
6981
void ImGui::TreePush(const void* ptr_id)
6982
{
6983
ImGuiWindow* window = GetCurrentWindow();
6984
Indent();
6985
window->DC.TreeDepth++;
6986
PushID(ptr_id);
6987
}
6988
6989
void ImGui::TreePushOverrideID(ImGuiID id)
6990
{
6991
ImGuiContext& g = *GImGui;
6992
ImGuiWindow* window = g.CurrentWindow;
6993
Indent();
6994
window->DC.TreeDepth++;
6995
PushOverrideID(id);
6996
}
6997
6998
void ImGui::TreePop()
6999
{
7000
ImGuiContext& g = *GImGui;
7001
ImGuiWindow* window = g.CurrentWindow;
7002
Unindent();
7003
7004
window->DC.TreeDepth--;
7005
ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
7006
7007
if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
7008
{
7009
const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7010
IM_ASSERT(data->ID == window->IDStack.back());
7011
7012
// Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
7013
if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
7014
if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
7015
NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
7016
7017
// Draw hierarchy lines
7018
if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
7019
TreeNodeDrawLineToTreePop(data);
7020
7021
g.TreeNodeStack.pop_back();
7022
window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
7023
window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
7024
}
7025
7026
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.
7027
PopID();
7028
}
7029
7030
// Horizontal distance preceding label when using TreeNode() or Bullet()
7031
float ImGui::GetTreeNodeToLabelSpacing()
7032
{
7033
ImGuiContext& g = *GImGui;
7034
return g.FontSize + (g.Style.FramePadding.x * 2.0f);
7035
}
7036
7037
// Set next TreeNode/CollapsingHeader open state.
7038
void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
7039
{
7040
ImGuiContext& g = *GImGui;
7041
if (g.CurrentWindow->SkipItems)
7042
return;
7043
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
7044
g.NextItemData.OpenVal = is_open;
7045
g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
7046
}
7047
7048
// Set next TreeNode/CollapsingHeader storage id.
7049
void ImGui::SetNextItemStorageID(ImGuiID storage_id)
7050
{
7051
ImGuiContext& g = *GImGui;
7052
if (g.CurrentWindow->SkipItems)
7053
return;
7054
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
7055
g.NextItemData.StorageId = storage_id;
7056
}
7057
7058
// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
7059
// 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().
7060
bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
7061
{
7062
ImGuiWindow* window = GetCurrentWindow();
7063
if (window->SkipItems)
7064
return false;
7065
ImGuiID id = window->GetID(label);
7066
return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
7067
}
7068
7069
// p_visible == NULL : regular collapsing header
7070
// 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
7071
// p_visible != NULL && *p_visible == false : do not show the header at all
7072
// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
7073
bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
7074
{
7075
ImGuiWindow* window = GetCurrentWindow();
7076
if (window->SkipItems)
7077
return false;
7078
7079
if (p_visible && !*p_visible)
7080
return false;
7081
7082
ImGuiID id = window->GetID(label);
7083
flags |= ImGuiTreeNodeFlags_CollapsingHeader;
7084
if (p_visible)
7085
flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
7086
bool is_open = TreeNodeBehavior(id, flags, label);
7087
if (p_visible != NULL)
7088
{
7089
// Create a small overlapping close button
7090
// FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
7091
// FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
7092
ImGuiContext& g = *GImGui;
7093
ImGuiLastItemData last_item_backup = g.LastItemData;
7094
float button_size = g.FontSize;
7095
float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
7096
float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
7097
ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
7098
if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
7099
*p_visible = false;
7100
g.LastItemData = last_item_backup;
7101
}
7102
7103
return is_open;
7104
}
7105
7106
//-------------------------------------------------------------------------
7107
// [SECTION] Widgets: Selectable
7108
//-------------------------------------------------------------------------
7109
// - Selectable()
7110
//-------------------------------------------------------------------------
7111
7112
// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
7113
// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
7114
// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
7115
// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
7116
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7117
{
7118
ImGuiWindow* window = GetCurrentWindow();
7119
if (window->SkipItems)
7120
return false;
7121
7122
ImGuiContext& g = *GImGui;
7123
const ImGuiStyle& style = g.Style;
7124
7125
// Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
7126
ImGuiID id = window->GetID(label);
7127
ImVec2 label_size = CalcTextSize(label, NULL, true);
7128
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
7129
ImVec2 pos = window->DC.CursorPos;
7130
pos.y += window->DC.CurrLineTextBaseOffset;
7131
ItemSize(size, 0.0f);
7132
7133
// Fill horizontal space
7134
// We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
7135
const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
7136
const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
7137
const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
7138
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
7139
size.x = ImMax(label_size.x, max_x - min_x);
7140
7141
// Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7142
// FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7143
ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7144
if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7145
{
7146
const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7147
const float spacing_y = style.ItemSpacing.y;
7148
const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7149
const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7150
bb.Min.x -= spacing_L;
7151
bb.Min.y -= spacing_U;
7152
bb.Max.x += (spacing_x - spacing_L);
7153
bb.Max.y += (spacing_y - spacing_U);
7154
}
7155
//if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7156
7157
const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7158
const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7159
bool is_visible;
7160
if (span_all_columns)
7161
{
7162
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7163
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7164
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7165
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7166
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7167
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7168
window->ClipRect.Min.x = backup_clip_rect_min_x;
7169
window->ClipRect.Max.x = backup_clip_rect_max_x;
7170
}
7171
else
7172
{
7173
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7174
}
7175
7176
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7177
if (!is_visible)
7178
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)
7179
return false;
7180
7181
const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7182
if (disabled_item && !disabled_global) // Only testing this as an optimization
7183
BeginDisabled();
7184
7185
// FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7186
// which would be advantageous since most selectable are not selected.
7187
if (span_all_columns)
7188
{
7189
if (g.CurrentTable)
7190
TablePushBackgroundChannel();
7191
else if (window->DC.CurrentColumns)
7192
PushColumnsBackground();
7193
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7194
g.LastItemData.ClipRect = window->ClipRect;
7195
}
7196
7197
// We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7198
ImGuiButtonFlags button_flags = 0;
7199
if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7200
if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7201
if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7202
if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7203
if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7204
if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7205
7206
// Multi-selection support (header)
7207
const bool was_selected = selected;
7208
if (is_multi_select)
7209
{
7210
// Handle multi-select + alter button flags for it
7211
MultiSelectItemHeader(id, &selected, &button_flags);
7212
}
7213
7214
bool hovered, held;
7215
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
7216
7217
// Multi-selection support (footer)
7218
if (is_multi_select)
7219
{
7220
MultiSelectItemFooter(id, &selected, &pressed);
7221
}
7222
else
7223
{
7224
// Auto-select when moved into
7225
// - This will be more fully fleshed in the range-select branch
7226
// - This is not exposed as it won't nicely work with some user side handling of shift/control
7227
// - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7228
// - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7229
// - (2) usage will fail with clipped items
7230
// The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7231
if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7232
if (g.NavJustMovedToId == id)
7233
selected = pressed = true;
7234
}
7235
7236
// Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7237
if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7238
{
7239
if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7240
{
7241
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7242
if (g.IO.ConfigNavCursorVisibleAuto)
7243
g.NavCursorVisible = false;
7244
}
7245
}
7246
if (pressed)
7247
MarkItemEdited(id);
7248
7249
if (selected != was_selected)
7250
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7251
7252
// Render
7253
if (is_visible)
7254
{
7255
const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7256
if (highlighted || selected)
7257
{
7258
// Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7259
ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7260
RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
7261
}
7262
if (g.NavId == id)
7263
{
7264
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7265
if (is_multi_select)
7266
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7267
RenderNavCursor(bb, id, nav_render_cursor_flags);
7268
}
7269
}
7270
7271
if (span_all_columns)
7272
{
7273
if (g.CurrentTable)
7274
TablePopBackgroundChannel();
7275
else if (window->DC.CurrentColumns)
7276
PopColumnsBackground();
7277
}
7278
7279
// Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7280
if (is_visible)
7281
RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb);
7282
7283
// Automatically close popups
7284
if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7285
CloseCurrentPopup();
7286
7287
if (disabled_item && !disabled_global)
7288
EndDisabled();
7289
7290
// Selectable() always returns a pressed state!
7291
// Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7292
// selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7293
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7294
return pressed; //-V1020
7295
}
7296
7297
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7298
{
7299
if (Selectable(label, *p_selected, flags, size_arg))
7300
{
7301
*p_selected = !*p_selected;
7302
return true;
7303
}
7304
return false;
7305
}
7306
7307
7308
//-------------------------------------------------------------------------
7309
// [SECTION] Widgets: Typing-Select support
7310
//-------------------------------------------------------------------------
7311
7312
// [Experimental] Currently not exposed in public API.
7313
// Consume character inputs and return search request, if any.
7314
// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7315
// if (ImGui::IsWindowFocused(...))
7316
// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7317
// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7318
// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7319
ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7320
{
7321
ImGuiContext& g = *GImGui;
7322
ImGuiTypingSelectState* data = &g.TypingSelectState;
7323
ImGuiTypingSelectRequest* out_request = &data->Request;
7324
7325
// Clear buffer
7326
const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7327
const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7328
if (data->SearchBuffer[0] != 0)
7329
{
7330
bool clear_buffer = false;
7331
clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7332
clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7333
clear_buffer |= g.NavAnyRequest;
7334
clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7335
clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7336
clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7337
//if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7338
if (clear_buffer)
7339
data->Clear();
7340
}
7341
7342
// Append to buffer
7343
const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7344
int buffer_len = (int)ImStrlen(data->SearchBuffer);
7345
bool select_request = false;
7346
for (ImWchar w : g.IO.InputQueueCharacters)
7347
{
7348
const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7349
if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7350
continue;
7351
char w_buf[5];
7352
ImTextCharToUtf8(w_buf, (unsigned int)w);
7353
if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7354
{
7355
select_request = true; // Same character: don't need to append to buffer.
7356
continue;
7357
}
7358
if (data->SingleCharModeLock)
7359
{
7360
data->Clear(); // Different character: clear
7361
buffer_len = 0;
7362
}
7363
memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7364
buffer_len += w_len;
7365
select_request = true;
7366
}
7367
g.IO.InputQueueCharacters.resize(0);
7368
7369
// Handle backspace
7370
if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7371
{
7372
char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7373
*p = 0;
7374
buffer_len = (int)(p - data->SearchBuffer);
7375
}
7376
7377
// Return request if any
7378
if (buffer_len == 0)
7379
return NULL;
7380
if (select_request)
7381
{
7382
data->FocusScope = g.NavFocusScopeId;
7383
data->LastRequestFrame = g.FrameCount;
7384
data->LastRequestTime = (float)g.Time;
7385
}
7386
out_request->Flags = flags;
7387
out_request->SearchBufferLen = buffer_len;
7388
out_request->SearchBuffer = data->SearchBuffer;
7389
out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7390
out_request->SingleCharMode = false;
7391
out_request->SingleCharSize = 0;
7392
7393
// Calculate if buffer contains the same character repeated.
7394
// - This can be used to implement a special search mode on first character.
7395
// - Performed on UTF-8 codepoint for correctness.
7396
// - SingleCharMode is always set for first input character, because it usually leads to a "next".
7397
if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7398
{
7399
const char* buf_begin = out_request->SearchBuffer;
7400
const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7401
const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7402
const char* p = buf_begin + c0_len;
7403
for (; p < buf_end; p += c0_len)
7404
if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7405
break;
7406
const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7407
out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7408
out_request->SingleCharSize = (ImS8)c0_len;
7409
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.
7410
}
7411
7412
return out_request;
7413
}
7414
7415
static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7416
{
7417
int match_len = 0;
7418
while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7419
match_len++;
7420
return match_len;
7421
}
7422
7423
// Default handler for finding a result for typing-select. You may implement your own.
7424
// You might want to display a tooltip to visualize the current request SearchBuffer
7425
// When SingleCharMode is set:
7426
// - it is better to NOT display a tooltip of other on-screen display indicator.
7427
// - the index of the currently focused item is required.
7428
// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7429
int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7430
{
7431
if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7432
return -1;
7433
int idx = -1;
7434
if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7435
idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7436
else
7437
idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7438
if (idx != -1)
7439
SetNavCursorVisibleAfterMove();
7440
return idx;
7441
}
7442
7443
// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7444
int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7445
{
7446
// FIXME: Assume selection user data is index. Would be extremely practical.
7447
//if (nav_item_idx == -1)
7448
// nav_item_idx = (int)g.NavLastValidSelectionUserData;
7449
7450
int first_match_idx = -1;
7451
bool return_next_match = false;
7452
for (int idx = 0; idx < items_count; idx++)
7453
{
7454
const char* item_name = get_item_name_func(user_data, idx);
7455
if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7456
continue;
7457
if (return_next_match) // Return next matching item after current item.
7458
return idx;
7459
if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7460
return idx;
7461
if (first_match_idx == -1) // Record first match for wrapping.
7462
first_match_idx = idx;
7463
if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7464
return_next_match = true;
7465
}
7466
return first_match_idx; // First result
7467
}
7468
7469
int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7470
{
7471
int longest_match_idx = -1;
7472
int longest_match_len = 0;
7473
for (int idx = 0; idx < items_count; idx++)
7474
{
7475
const char* item_name = get_item_name_func(user_data, idx);
7476
const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7477
if (match_len <= longest_match_len)
7478
continue;
7479
longest_match_idx = idx;
7480
longest_match_len = match_len;
7481
if (match_len == req->SearchBufferLen)
7482
break;
7483
}
7484
return longest_match_idx;
7485
}
7486
7487
void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7488
{
7489
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7490
Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7491
Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7492
Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7493
#else
7494
IM_UNUSED(data);
7495
#endif
7496
}
7497
7498
//-------------------------------------------------------------------------
7499
// [SECTION] Widgets: Box-Select support
7500
// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7501
//-------------------------------------------------------------------------
7502
// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7503
//-------------------------------------------------------------------------
7504
// - BoxSelectPreStartDrag() [Internal]
7505
// - BoxSelectActivateDrag() [Internal]
7506
// - BoxSelectDeactivateDrag() [Internal]
7507
// - BoxSelectScrollWithMouseDrag() [Internal]
7508
// - BeginBoxSelect() [Internal]
7509
// - EndBoxSelect() [Internal]
7510
//-------------------------------------------------------------------------
7511
7512
// Call on the initial click.
7513
static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7514
{
7515
ImGuiContext& g = *GImGui;
7516
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7517
bs->ID = id;
7518
bs->IsStarting = true; // Consider starting box-select.
7519
bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7520
bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7521
bs->KeyMods = g.IO.KeyMods;
7522
bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7523
bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7524
}
7525
7526
static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7527
{
7528
ImGuiContext& g = *GImGui;
7529
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7530
bs->IsActive = true;
7531
bs->Window = window;
7532
bs->IsStarting = false;
7533
ImGui::SetActiveID(bs->ID, window);
7534
ImGui::SetActiveIdUsingAllKeyboardKeys();
7535
if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7536
bs->RequestClear = true;
7537
}
7538
7539
static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7540
{
7541
ImGuiContext& g = *GImGui;
7542
bs->IsActive = bs->IsStarting = false;
7543
if (g.ActiveId == bs->ID)
7544
{
7545
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7546
ImGui::ClearActiveID();
7547
}
7548
bs->ID = 0;
7549
}
7550
7551
static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7552
{
7553
ImGuiContext& g = *GImGui;
7554
IM_ASSERT(bs->Window == window);
7555
for (int n = 0; n < 2; n++) // each axis
7556
{
7557
const float mouse_pos = g.IO.MousePos[n];
7558
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;
7559
const float scroll_curr = window->Scroll[n];
7560
if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7561
continue;
7562
7563
const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7564
const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7565
bs->ScrollAccum[n] += scroll_step;
7566
7567
// Accumulate into a stored value so we can handle high-framerate
7568
const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7569
if (scroll_step_i == 0.0f)
7570
continue;
7571
if (n == 0)
7572
ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7573
else
7574
ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7575
bs->ScrollAccum[n] -= scroll_step_i;
7576
}
7577
}
7578
7579
bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7580
{
7581
ImGuiContext& g = *GImGui;
7582
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7583
KeepAliveID(box_select_id);
7584
if (bs->ID != box_select_id)
7585
return false;
7586
7587
// IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7588
bs->UnclipMode = false;
7589
bs->RequestClear = false;
7590
if (bs->IsStarting && IsMouseDragPastThreshold(0))
7591
BoxSelectActivateDrag(bs, window);
7592
else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7593
BoxSelectDeactivateDrag(bs);
7594
if (!bs->IsActive)
7595
return false;
7596
7597
// Current frame absolute prev/current rectangles are used to toggle selection.
7598
// They are derived from positions relative to scrolling space.
7599
ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7600
ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7601
ImVec2 curr_end_pos_abs = g.IO.MousePos;
7602
if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7603
curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7604
bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7605
bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7606
bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7607
bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7608
7609
// Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7610
// Storing an extra rect used by widgets supporting box-select.
7611
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7612
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7613
{
7614
bs->UnclipMode = true;
7615
bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7616
bs->UnclipRect.Add(bs->BoxSelectRectCurr);
7617
}
7618
7619
//GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7620
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7621
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7622
return true;
7623
}
7624
7625
void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7626
{
7627
ImGuiContext& g = *GImGui;
7628
ImGuiWindow* window = g.CurrentWindow;
7629
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7630
IM_ASSERT(bs->IsActive);
7631
bs->UnclipMode = false;
7632
7633
// Render selection rectangle
7634
bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7635
ImRect box_select_r = bs->BoxSelectRectCurr;
7636
box_select_r.ClipWith(scope_rect);
7637
window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7638
window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
7639
7640
// Scroll
7641
const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7642
if (enable_scroll)
7643
{
7644
ImRect scroll_r = scope_rect;
7645
scroll_r.Expand(-g.FontSize);
7646
//GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7647
if (!scroll_r.Contains(g.IO.MousePos))
7648
BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7649
}
7650
}
7651
7652
//-------------------------------------------------------------------------
7653
// [SECTION] Widgets: Multi-Select support
7654
//-------------------------------------------------------------------------
7655
// - DebugLogMultiSelectRequests() [Internal]
7656
// - CalcScopeRect() [Internal]
7657
// - BeginMultiSelect()
7658
// - EndMultiSelect()
7659
// - SetNextItemSelectionUserData()
7660
// - MultiSelectItemHeader() [Internal]
7661
// - MultiSelectItemFooter() [Internal]
7662
// - DebugNodeMultiSelectState() [Internal]
7663
//-------------------------------------------------------------------------
7664
7665
static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7666
{
7667
ImGuiContext& g = *GImGui;
7668
IM_UNUSED(function);
7669
for (const ImGuiSelectionRequest& req : io->Requests)
7670
{
7671
if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7672
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);
7673
}
7674
}
7675
7676
static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7677
{
7678
ImGuiContext& g = *GImGui;
7679
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7680
{
7681
// Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7682
return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
7683
}
7684
else
7685
{
7686
// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7687
ImRect scope_rect = window->InnerClipRect;
7688
if (g.CurrentTable != NULL)
7689
scope_rect = g.CurrentTable->HostClipRect;
7690
7691
// Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7692
scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
7693
return scope_rect;
7694
}
7695
}
7696
7697
// Return ImGuiMultiSelectIO structure.
7698
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7699
// Passing 'selection_size' and 'items_count' parameters is currently optional.
7700
// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7701
// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7702
// - '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).
7703
// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7704
// - 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.
7705
ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7706
{
7707
ImGuiContext& g = *GImGui;
7708
ImGuiWindow* window = g.CurrentWindow;
7709
7710
if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7711
g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
7712
ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7713
IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7714
g.CurrentMultiSelect = ms;
7715
if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7716
flags |= ImGuiMultiSelectFlags_ScopeWindow;
7717
if (flags & ImGuiMultiSelectFlags_SingleSelect)
7718
flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7719
if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7720
flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7721
7722
// FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
7723
// They should perhaps be stacked properly?
7724
if (ImGuiTable* table = g.CurrentTable)
7725
if (table->CurrentColumn != -1)
7726
TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
7727
7728
// FIXME: BeginFocusScope()
7729
const ImGuiID id = window->IDStack.back();
7730
ms->Clear();
7731
ms->FocusScopeId = id;
7732
ms->Flags = flags;
7733
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7734
ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7735
ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7736
PushFocusScope(ms->FocusScopeId);
7737
if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7738
window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7739
7740
// Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7741
ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7742
if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7743
ms->KeyMods &= ~ImGuiMod_Shift;
7744
7745
// Bind storage
7746
ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
7747
storage->ID = id;
7748
storage->LastFrameActive = g.FrameCount;
7749
storage->LastSelectionSize = selection_size;
7750
storage->Window = window;
7751
ms->Storage = storage;
7752
7753
// Output to user
7754
ms->IO.Requests.resize(0);
7755
ms->IO.RangeSrcItem = storage->RangeSrcItem;
7756
ms->IO.NavIdItem = storage->NavIdItem;
7757
ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7758
ms->IO.ItemsCount = items_count;
7759
7760
// Clear when using Navigation to move within the scope
7761
// (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7762
bool request_clear = false;
7763
bool request_select_all = false;
7764
if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7765
{
7766
if (ms->KeyMods & ImGuiMod_Shift)
7767
ms->IsKeyboardSetRange = true;
7768
if (ms->IsKeyboardSetRange)
7769
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7770
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7771
request_clear = true;
7772
}
7773
else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7774
{
7775
// Also clear on leaving scope (may be optional?)
7776
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7777
request_clear = true;
7778
}
7779
7780
// Box-select handling: update active state.
7781
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7782
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7783
{
7784
ms->BoxSelectId = GetID("##BoxSelect");
7785
if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
7786
request_clear |= bs->RequestClear;
7787
}
7788
7789
if (ms->IsFocused)
7790
{
7791
// Shortcut: Clear selection (Escape)
7792
// - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7793
// - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7794
if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7795
{
7796
if (selection_size != 0 || bs->IsActive)
7797
if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
7798
{
7799
request_clear = true;
7800
if (bs->IsActive)
7801
BoxSelectDeactivateDrag(bs);
7802
}
7803
}
7804
7805
// Shortcut: Select all (CTRL+A)
7806
if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7807
if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
7808
request_select_all = true;
7809
}
7810
7811
if (request_clear || request_select_all)
7812
{
7813
MultiSelectAddSetAll(ms, request_select_all);
7814
if (!request_select_all)
7815
storage->LastSelectionSize = 0;
7816
}
7817
ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7818
ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7819
7820
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7821
DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
7822
7823
return &ms->IO;
7824
}
7825
7826
// Return updated ImGuiMultiSelectIO structure.
7827
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7828
ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7829
{
7830
ImGuiContext& g = *GImGui;
7831
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7832
ImGuiMultiSelectState* storage = ms->Storage;
7833
ImGuiWindow* window = g.CurrentWindow;
7834
IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
7835
IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7836
IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7837
7838
ImRect scope_rect = CalcScopeRect(ms, window);
7839
if (ms->IsFocused)
7840
{
7841
// We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7842
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)
7843
{
7844
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7845
storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7846
}
7847
if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7848
{
7849
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7850
storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7851
storage->NavIdSelected = -1;
7852
}
7853
7854
if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
7855
EndBoxSelect(scope_rect, ms->Flags);
7856
}
7857
7858
if (ms->IsEndIO == false)
7859
ms->IO.Requests.resize(0);
7860
7861
// Clear selection when clicking void?
7862
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7863
// The InnerRect test is necessary for non-child/decorated windows.
7864
bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos);
7865
if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7866
scope_hovered &= scope_rect.Contains(g.IO.MousePos);
7867
if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7868
{
7869
if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7870
{
7871
if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7872
{
7873
BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7874
FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
7875
SetHoveredID(ms->BoxSelectId);
7876
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7877
SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
7878
}
7879
}
7880
7881
if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7882
if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
7883
MultiSelectAddSetAll(ms, false);
7884
}
7885
7886
// Courtesy nav wrapping helper flag
7887
if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7888
{
7889
IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7890
ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
7891
}
7892
7893
// Unwind
7894
window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
7895
PopFocusScope();
7896
7897
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7898
DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
7899
7900
ms->FocusScopeId = 0;
7901
ms->Flags = ImGuiMultiSelectFlags_None;
7902
g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7903
7904
return &ms->IO;
7905
}
7906
7907
void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7908
{
7909
// Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7910
// This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7911
ImGuiContext& g = *GImGui;
7912
g.NextItemData.SelectionUserData = selection_user_data;
7913
g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7914
7915
if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7916
{
7917
// Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7918
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7919
if (ms->IO.RangeSrcItem == selection_user_data)
7920
ms->RangeSrcPassedBy = true;
7921
}
7922
else
7923
{
7924
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7925
}
7926
}
7927
7928
// In charge of:
7929
// - Applying SetAll for submitted items.
7930
// - Applying SetRange for submitted items and record end points.
7931
// - Altering button behavior flags to facilitate use with drag and drop.
7932
void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7933
{
7934
ImGuiContext& g = *GImGui;
7935
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7936
7937
bool selected = *p_selected;
7938
if (ms->IsFocused)
7939
{
7940
ImGuiMultiSelectState* storage = ms->Storage;
7941
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7942
IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7943
7944
// Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7945
// This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7946
// If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7947
if (ms->LoopRequestSetAll != -1)
7948
selected = (ms->LoopRequestSetAll == 1);
7949
7950
// 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)
7951
// For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7952
if (ms->IsKeyboardSetRange)
7953
{
7954
IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7955
const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7956
if (is_range_dst)
7957
ms->RangeDstPassedBy = true;
7958
if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7959
{
7960
storage->RangeSrcItem = item_data;
7961
storage->RangeSelected = selected ? 1 : 0;
7962
}
7963
const bool is_range_src = storage->RangeSrcItem == item_data;
7964
if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7965
{
7966
// Apply range-select value to visible items
7967
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7968
selected = (storage->RangeSelected != 0);
7969
}
7970
else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7971
{
7972
// Clear other items
7973
selected = false;
7974
}
7975
}
7976
*p_selected = selected;
7977
}
7978
7979
// Alter button behavior flags
7980
// To handle drag and drop of multiple items we need to avoid clearing selection on click.
7981
// 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.
7982
if (p_button_flags != NULL)
7983
{
7984
ImGuiButtonFlags button_flags = *p_button_flags;
7985
button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7986
if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7987
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7988
else
7989
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7990
*p_button_flags = button_flags;
7991
}
7992
}
7993
7994
// In charge of:
7995
// - Auto-select on navigation.
7996
// - Box-select toggle handling.
7997
// - Right-click handling.
7998
// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
7999
// - Record current selection state for RangeSrc
8000
// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
8001
void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
8002
{
8003
ImGuiContext& g = *GImGui;
8004
ImGuiWindow* window = g.CurrentWindow;
8005
8006
bool selected = *p_selected;
8007
bool pressed = *p_pressed;
8008
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8009
ImGuiMultiSelectState* storage = ms->Storage;
8010
if (pressed)
8011
ms->IsFocused = true;
8012
8013
bool hovered = false;
8014
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
8015
hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8016
if (!ms->IsFocused && !hovered)
8017
return;
8018
8019
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8020
8021
ImGuiMultiSelectFlags flags = ms->Flags;
8022
const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
8023
bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
8024
bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
8025
8026
bool apply_to_range_src = false;
8027
8028
if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8029
apply_to_range_src = true;
8030
if (ms->IsEndIO == false)
8031
{
8032
ms->IO.Requests.resize(0);
8033
ms->IsEndIO = true;
8034
}
8035
8036
// Auto-select as you navigate a list
8037
if (g.NavJustMovedToId == id)
8038
{
8039
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8040
{
8041
if (is_ctrl && is_shift)
8042
pressed = true;
8043
else if (!is_ctrl)
8044
selected = pressed = true;
8045
}
8046
else
8047
{
8048
// With NoAutoSelect, using Shift+keyboard performs a write/copy
8049
if (is_shift)
8050
pressed = true;
8051
else if (!is_ctrl)
8052
apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
8053
}
8054
}
8055
8056
if (apply_to_range_src)
8057
{
8058
storage->RangeSrcItem = item_data;
8059
storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
8060
}
8061
8062
// Box-select toggle handling
8063
if (ms->BoxSelectId != 0)
8064
if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
8065
{
8066
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect);
8067
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect);
8068
if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
8069
{
8070
if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
8071
{
8072
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)
8073
bs->IsStartedSetNavIdOnce = false;
8074
}
8075
else
8076
{
8077
selected = !selected;
8078
MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
8079
}
8080
storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
8081
}
8082
}
8083
8084
// Right-click handling.
8085
// FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
8086
if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8087
{
8088
if (g.ActiveId != 0 && g.ActiveId != id)
8089
ClearActiveID();
8090
SetFocusID(id, window);
8091
if (!pressed && !selected)
8092
{
8093
pressed = true;
8094
is_ctrl = is_shift = false;
8095
}
8096
}
8097
8098
// Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
8099
// The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
8100
// (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
8101
const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
8102
8103
// Alter selection
8104
if (pressed && (!enter_pressed || !selected))
8105
{
8106
// Box-select
8107
ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
8108
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8109
if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
8110
BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
8111
8112
//----------------------------------------------------------------------------------------
8113
// ACTION | Begin | Pressed/Activated | End
8114
//----------------------------------------------------------------------------------------
8115
// Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
8116
// Keys Navigated: Ctrl | n/a | n/a
8117
// Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8118
// Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
8119
// Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
8120
// Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
8121
// Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
8122
//----------------------------------------------------------------------------------------
8123
// Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
8124
// Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
8125
// Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8126
// Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
8127
//----------------------------------------------------------------------------------------
8128
8129
if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8130
{
8131
bool request_clear = false;
8132
if (is_singleselect)
8133
request_clear = true;
8134
else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
8135
request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
8136
else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
8137
request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
8138
if (request_clear)
8139
MultiSelectAddSetAll(ms, false);
8140
}
8141
8142
int range_direction;
8143
bool range_selected;
8144
if (is_shift && !is_singleselect)
8145
{
8146
//IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8147
if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8148
storage->RangeSrcItem = item_data;
8149
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8150
{
8151
// Shift+Arrow always select
8152
// Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8153
range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8154
}
8155
else
8156
{
8157
// Shift+Arrow copy source selection state
8158
// Shift+Click always copy from target selection state
8159
if (ms->IsKeyboardSetRange)
8160
range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8161
else
8162
range_selected = !selected;
8163
}
8164
range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8165
}
8166
else
8167
{
8168
// Ctrl inverts selection, otherwise always select
8169
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8170
selected = is_ctrl ? !selected : true;
8171
else
8172
selected = !selected;
8173
storage->RangeSrcItem = item_data;
8174
range_selected = selected;
8175
range_direction = +1;
8176
}
8177
MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
8178
}
8179
8180
// Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
8181
if (storage->RangeSrcItem == item_data)
8182
storage->RangeSelected = selected ? 1 : 0;
8183
8184
// Update/store the selection state of focused item
8185
if (g.NavId == id)
8186
{
8187
storage->NavIdItem = item_data;
8188
storage->NavIdSelected = selected ? 1 : 0;
8189
}
8190
if (storage->NavIdItem == item_data)
8191
ms->NavIdPassedBy = true;
8192
ms->LastSubmittedItem = item_data;
8193
8194
*p_selected = selected;
8195
*p_pressed = pressed;
8196
}
8197
8198
void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8199
{
8200
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8201
ms->IO.Requests.resize(0); // Can always clear previous requests
8202
ms->IO.Requests.push_back(req); // Add new request
8203
}
8204
8205
void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8206
{
8207
// Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8208
if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8209
{
8210
ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8211
if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
8212
{
8213
prev->RangeLastItem = last_item;
8214
return;
8215
}
8216
}
8217
8218
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
8219
ms->IO.Requests.push_back(req); // Add new request
8220
}
8221
8222
void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8223
{
8224
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8225
const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8226
if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8227
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*");
8228
if (!is_active) { PopStyleColor(); }
8229
if (!open)
8230
return;
8231
Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8232
Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8233
Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8234
TreePop();
8235
#else
8236
IM_UNUSED(storage);
8237
#endif
8238
}
8239
8240
//-------------------------------------------------------------------------
8241
// [SECTION] Widgets: Multi-Select helpers
8242
//-------------------------------------------------------------------------
8243
// - ImGuiSelectionBasicStorage
8244
// - ImGuiSelectionExternalStorage
8245
//-------------------------------------------------------------------------
8246
8247
ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8248
{
8249
Size = 0;
8250
PreserveOrder = false;
8251
UserData = NULL;
8252
AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8253
_SelectionOrder = 1; // Always >0
8254
}
8255
8256
void ImGuiSelectionBasicStorage::Clear()
8257
{
8258
Size = 0;
8259
_SelectionOrder = 1; // Always >0
8260
_Storage.Data.resize(0);
8261
}
8262
8263
void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8264
{
8265
ImSwap(Size, r.Size);
8266
ImSwap(_SelectionOrder, r._SelectionOrder);
8267
_Storage.Data.swap(r._Storage.Data);
8268
}
8269
8270
bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8271
{
8272
return _Storage.GetInt(id, 0) != 0;
8273
}
8274
8275
static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8276
{
8277
int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8278
int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8279
return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8280
}
8281
8282
// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8283
// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8284
bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8285
{
8286
ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8287
ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8288
if (PreserveOrder && it == NULL && it_end != NULL)
8289
ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8290
if (it == NULL)
8291
it = _Storage.Data.Data;
8292
IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8293
if (it != it_end)
8294
while (it->val_i == 0 && it < it_end)
8295
it++;
8296
const bool has_more = (it != it_end);
8297
*opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8298
*out_id = has_more ? it->key : 0;
8299
if (PreserveOrder && !has_more)
8300
_Storage.BuildSortByKey();
8301
return has_more;
8302
}
8303
8304
void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8305
{
8306
int* p_int = _Storage.GetIntRef(id, 0);
8307
if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8308
else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8309
}
8310
8311
// Optimized for batch edits (with same value of 'selected')
8312
static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8313
{
8314
ImGuiStorage* storage = &selection->_Storage;
8315
ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8316
const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8317
if (selected == (is_contained && it->val_i != 0))
8318
return;
8319
if (selected && !is_contained)
8320
storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8321
else if (is_contained)
8322
it->val_i = selected ? selection_order : 0; // Modify in-place.
8323
selection->Size += selected ? +1 : -1;
8324
}
8325
8326
static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8327
{
8328
ImGuiStorage* storage = &selection->_Storage;
8329
if (selected && selection->Size != size_before_amends)
8330
storage->BuildSortByKey(); // When done selecting: sort everything
8331
}
8332
8333
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8334
// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8335
// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8336
// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8337
// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8338
// a lookup in order to have some way to iterate/interpolate between two items.
8339
// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8340
// and constructing a view index <> object id/ptr data structure anyway.
8341
// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8342
// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8343
// The most simple implementation (using indices everywhere) would look like:
8344
// for (ImGuiSelectionRequest& req : ms_io->Requests)
8345
// {
8346
// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8347
// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8348
// }
8349
void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8350
{
8351
// For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8352
// It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8353
// Other scheme may handle SetAll differently.
8354
IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8355
IM_ASSERT(AdapterIndexToStorageId != NULL);
8356
8357
// This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8358
// - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8359
// - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8360
// - 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.
8361
// - 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
8362
// left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8363
// FIXME-OPT: For each block of consecutive SetRange request:
8364
// - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8365
// - rewrite sorted storage a single time.
8366
for (ImGuiSelectionRequest& req : ms_io->Requests)
8367
{
8368
if (req.Type == ImGuiSelectionRequestType_SetAll)
8369
{
8370
Clear();
8371
if (req.Selected)
8372
{
8373
_Storage.Data.reserve(ms_io->ItemsCount);
8374
const int size_before_amends = _Storage.Data.Size;
8375
for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8376
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8377
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8378
}
8379
}
8380
else if (req.Type == ImGuiSelectionRequestType_SetRange)
8381
{
8382
const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8383
//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);
8384
if (selection_changes == 1 || (selection_changes < Size / 100))
8385
{
8386
// Multiple sorted insertion + copy likely to be faster.
8387
// Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8388
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8389
SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8390
}
8391
else
8392
{
8393
// Append insertion + single sort likely be faster.
8394
// Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8395
const int size_before_amends = _Storage.Data.Size;
8396
int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8397
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8398
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8399
if (req.Selected)
8400
_SelectionOrder += selection_changes;
8401
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8402
}
8403
}
8404
}
8405
}
8406
8407
//-------------------------------------------------------------------------
8408
8409
ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8410
{
8411
UserData = NULL;
8412
AdapterSetItemSelected = NULL;
8413
}
8414
8415
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8416
// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8417
// This makes no assumption about underlying storage.
8418
void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8419
{
8420
IM_ASSERT(AdapterSetItemSelected);
8421
for (ImGuiSelectionRequest& req : ms_io->Requests)
8422
{
8423
if (req.Type == ImGuiSelectionRequestType_SetAll)
8424
for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8425
AdapterSetItemSelected(this, idx, req.Selected);
8426
if (req.Type == ImGuiSelectionRequestType_SetRange)
8427
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8428
AdapterSetItemSelected(this, idx, req.Selected);
8429
}
8430
}
8431
8432
//-------------------------------------------------------------------------
8433
// [SECTION] Widgets: ListBox
8434
//-------------------------------------------------------------------------
8435
// - BeginListBox()
8436
// - EndListBox()
8437
// - ListBox()
8438
//-------------------------------------------------------------------------
8439
8440
// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8441
// This handle some subtleties with capturing info from the label.
8442
// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8443
// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8444
// 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).
8445
bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8446
{
8447
ImGuiContext& g = *GImGui;
8448
ImGuiWindow* window = GetCurrentWindow();
8449
if (window->SkipItems)
8450
return false;
8451
8452
const ImGuiStyle& style = g.Style;
8453
const ImGuiID id = GetID(label);
8454
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8455
8456
// Size default to hold ~7.25 items.
8457
// Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8458
ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8459
ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8460
ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8461
ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8462
g.NextItemData.ClearFlags();
8463
8464
if (!IsRectVisible(bb.Min, bb.Max))
8465
{
8466
ItemSize(bb.GetSize(), style.FramePadding.y);
8467
ItemAdd(bb, 0, &frame_bb);
8468
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8469
return false;
8470
}
8471
8472
// FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8473
BeginGroup();
8474
if (label_size.x > 0.0f)
8475
{
8476
ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8477
RenderText(label_pos, label);
8478
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8479
AlignTextToFramePadding();
8480
}
8481
8482
BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8483
return true;
8484
}
8485
8486
void ImGui::EndListBox()
8487
{
8488
ImGuiContext& g = *GImGui;
8489
ImGuiWindow* window = g.CurrentWindow;
8490
IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8491
IM_UNUSED(window);
8492
8493
EndChild();
8494
EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8495
}
8496
8497
bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8498
{
8499
const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8500
return value_changed;
8501
}
8502
8503
// This is merely a helper around BeginListBox(), EndListBox().
8504
// Considering using those directly to submit custom data or store selection differently.
8505
bool 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)
8506
{
8507
ImGuiContext& g = *GImGui;
8508
8509
// Calculate size from "height_in_items"
8510
if (height_in_items < 0)
8511
height_in_items = ImMin(items_count, 7);
8512
float height_in_items_f = height_in_items + 0.25f;
8513
ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8514
8515
if (!BeginListBox(label, size))
8516
return false;
8517
8518
// Assume all items have even height (= 1 line of text). If you need items of different height,
8519
// you can create a custom version of ListBox() in your code without using the clipper.
8520
bool value_changed = false;
8521
ImGuiListClipper clipper;
8522
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.
8523
clipper.IncludeItemByIndex(*current_item);
8524
while (clipper.Step())
8525
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8526
{
8527
const char* item_text = getter(user_data, i);
8528
if (item_text == NULL)
8529
item_text = "*Unknown item*";
8530
8531
PushID(i);
8532
const bool item_selected = (i == *current_item);
8533
if (Selectable(item_text, item_selected))
8534
{
8535
*current_item = i;
8536
value_changed = true;
8537
}
8538
if (item_selected)
8539
SetItemDefaultFocus();
8540
PopID();
8541
}
8542
EndListBox();
8543
8544
if (value_changed)
8545
MarkItemEdited(g.LastItemData.ID);
8546
8547
return value_changed;
8548
}
8549
8550
//-------------------------------------------------------------------------
8551
// [SECTION] Widgets: PlotLines, PlotHistogram
8552
//-------------------------------------------------------------------------
8553
// - PlotEx() [Internal]
8554
// - PlotLines()
8555
// - PlotHistogram()
8556
//-------------------------------------------------------------------------
8557
// Plot/Graph widgets are not very good.
8558
// Consider writing your own, or using a third-party one, see:
8559
// - ImPlot https://github.com/epezent/implot
8560
// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8561
//-------------------------------------------------------------------------
8562
8563
int 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)
8564
{
8565
ImGuiContext& g = *GImGui;
8566
ImGuiWindow* window = GetCurrentWindow();
8567
if (window->SkipItems)
8568
return -1;
8569
8570
const ImGuiStyle& style = g.Style;
8571
const ImGuiID id = window->GetID(label);
8572
8573
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8574
const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8575
8576
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8577
const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8578
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));
8579
ItemSize(total_bb, style.FramePadding.y);
8580
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8581
return -1;
8582
bool hovered;
8583
ButtonBehavior(frame_bb, id, &hovered, NULL);
8584
8585
// Determine scale from values if not specified
8586
if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8587
{
8588
float v_min = FLT_MAX;
8589
float v_max = -FLT_MAX;
8590
for (int i = 0; i < values_count; i++)
8591
{
8592
const float v = values_getter(data, i);
8593
if (v != v) // Ignore NaN values
8594
continue;
8595
v_min = ImMin(v_min, v);
8596
v_max = ImMax(v_max, v);
8597
}
8598
if (scale_min == FLT_MAX)
8599
scale_min = v_min;
8600
if (scale_max == FLT_MAX)
8601
scale_max = v_max;
8602
}
8603
8604
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8605
8606
const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8607
int idx_hovered = -1;
8608
if (values_count >= values_count_min)
8609
{
8610
int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8611
int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8612
8613
// Tooltip on hover
8614
if (hovered && inner_bb.Contains(g.IO.MousePos))
8615
{
8616
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
8617
const int v_idx = (int)(t * item_count);
8618
IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8619
8620
const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8621
const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8622
if (plot_type == ImGuiPlotType_Lines)
8623
SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8624
else if (plot_type == ImGuiPlotType_Histogram)
8625
SetTooltip("%d: %8.4g", v_idx, v0);
8626
idx_hovered = v_idx;
8627
}
8628
8629
const float t_step = 1.0f / (float)res_w;
8630
const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8631
8632
float v0 = values_getter(data, (0 + values_offset) % values_count);
8633
float t0 = 0.0f;
8634
ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8635
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
8636
8637
const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8638
const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8639
8640
for (int n = 0; n < res_w; n++)
8641
{
8642
const float t1 = t0 + t_step;
8643
const int v1_idx = (int)(t0 * item_count + 0.5f);
8644
IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8645
const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8646
const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
8647
8648
// 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.
8649
ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
8650
ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8651
if (plot_type == ImGuiPlotType_Lines)
8652
{
8653
window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8654
}
8655
else if (plot_type == ImGuiPlotType_Histogram)
8656
{
8657
if (pos1.x >= pos0.x + 2.0f)
8658
pos1.x -= 1.0f;
8659
window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8660
}
8661
8662
t0 = t1;
8663
tp0 = tp1;
8664
}
8665
}
8666
8667
// Text overlay
8668
if (overlay_text)
8669
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));
8670
8671
if (label_size.x > 0.0f)
8672
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label);
8673
8674
// Return hovered index or -1 if none are hovered.
8675
// 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().
8676
return idx_hovered;
8677
}
8678
8679
struct ImGuiPlotArrayGetterData
8680
{
8681
const float* Values;
8682
int Stride;
8683
8684
ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8685
};
8686
8687
static float Plot_ArrayGetter(void* data, int idx)
8688
{
8689
ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8690
const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8691
return v;
8692
}
8693
8694
void 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)
8695
{
8696
ImGuiPlotArrayGetterData data(values, stride);
8697
PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8698
}
8699
8700
void 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)
8701
{
8702
PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8703
}
8704
8705
void 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)
8706
{
8707
ImGuiPlotArrayGetterData data(values, stride);
8708
PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8709
}
8710
8711
void 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)
8712
{
8713
PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8714
}
8715
8716
//-------------------------------------------------------------------------
8717
// [SECTION] Widgets: Value helpers
8718
// Those is not very useful, legacy API.
8719
//-------------------------------------------------------------------------
8720
// - Value()
8721
//-------------------------------------------------------------------------
8722
8723
void ImGui::Value(const char* prefix, bool b)
8724
{
8725
Text("%s: %s", prefix, (b ? "true" : "false"));
8726
}
8727
8728
void ImGui::Value(const char* prefix, int v)
8729
{
8730
Text("%s: %d", prefix, v);
8731
}
8732
8733
void ImGui::Value(const char* prefix, unsigned int v)
8734
{
8735
Text("%s: %d", prefix, v);
8736
}
8737
8738
void ImGui::Value(const char* prefix, float v, const char* float_format)
8739
{
8740
if (float_format)
8741
{
8742
char fmt[64];
8743
ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format);
8744
Text(fmt, prefix, v);
8745
}
8746
else
8747
{
8748
Text("%s: %.3f", prefix, v);
8749
}
8750
}
8751
8752
//-------------------------------------------------------------------------
8753
// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8754
//-------------------------------------------------------------------------
8755
// - ImGuiMenuColumns [Internal]
8756
// - BeginMenuBar()
8757
// - EndMenuBar()
8758
// - BeginMainMenuBar()
8759
// - EndMainMenuBar()
8760
// - BeginMenu()
8761
// - EndMenu()
8762
// - MenuItemEx() [Internal]
8763
// - MenuItem()
8764
//-------------------------------------------------------------------------
8765
8766
// Helpers for internal use
8767
void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8768
{
8769
if (window_reappearing)
8770
memset(Widths, 0, sizeof(Widths));
8771
Spacing = (ImU16)spacing;
8772
CalcNextTotalWidth(true);
8773
memset(Widths, 0, sizeof(Widths));
8774
TotalWidth = NextTotalWidth;
8775
NextTotalWidth = 0;
8776
}
8777
8778
void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8779
{
8780
ImU16 offset = 0;
8781
bool want_spacing = false;
8782
for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8783
{
8784
ImU16 width = Widths[i];
8785
if (want_spacing && width > 0)
8786
offset += Spacing;
8787
want_spacing |= (width > 0);
8788
if (update_offsets)
8789
{
8790
if (i == 1) { OffsetLabel = offset; }
8791
if (i == 2) { OffsetShortcut = offset; }
8792
if (i == 3) { OffsetMark = offset; }
8793
}
8794
offset += width;
8795
}
8796
NextTotalWidth = offset;
8797
}
8798
8799
float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8800
{
8801
Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
8802
Widths[1] = ImMax(Widths[1], (ImU16)w_label);
8803
Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
8804
Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
8805
CalcNextTotalWidth(false);
8806
return (float)ImMax(TotalWidth, NextTotalWidth);
8807
}
8808
8809
// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8810
// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8811
// 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.
8812
// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8813
bool ImGui::BeginMenuBar()
8814
{
8815
ImGuiWindow* window = GetCurrentWindow();
8816
if (window->SkipItems)
8817
return false;
8818
if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8819
return false;
8820
8821
IM_ASSERT(!window->DC.MenuBarAppending);
8822
BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8823
PushID("##MenuBar");
8824
8825
// 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.
8826
// 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.
8827
const float border_top = ImMax(window->WindowBorderSize * 0.5f - window->TitleBarHeight, 0.0f);
8828
ImRect bar_rect = window->MenuBarRect();
8829
ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize * 0.5f), IM_ROUND(bar_rect.Min.y + border_top), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize * 0.5f))), IM_ROUND(bar_rect.Max.y));
8830
clip_rect.ClipWith(window->OuterRectClipped);
8831
PushClipRect(clip_rect.Min, clip_rect.Max, false);
8832
8833
// 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).
8834
window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8835
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8836
window->DC.IsSameLine = false;
8837
window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8838
window->DC.MenuBarAppending = true;
8839
AlignTextToFramePadding();
8840
return true;
8841
}
8842
8843
void ImGui::EndMenuBar()
8844
{
8845
ImGuiWindow* window = GetCurrentWindow();
8846
if (window->SkipItems)
8847
return;
8848
ImGuiContext& g = *GImGui;
8849
8850
IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8851
IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8852
IM_ASSERT(window->DC.MenuBarAppending);
8853
8854
// Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8855
if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8856
{
8857
// Try to find out if the request is for one of our child menu
8858
ImGuiWindow* nav_earliest_child = g.NavWindow;
8859
while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8860
nav_earliest_child = nav_earliest_child->ParentWindow;
8861
if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8862
{
8863
// To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8864
// 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)
8865
const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8866
IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8867
FocusWindow(window);
8868
SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
8869
// FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
8870
if (g.NavCursorVisible)
8871
{
8872
g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
8873
g.NavCursorHideFrames = 2;
8874
}
8875
g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
8876
NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
8877
}
8878
}
8879
8880
PopClipRect();
8881
PopID();
8882
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.
8883
8884
// FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8885
ImGuiGroupData& group_data = g.GroupStack.back();
8886
group_data.EmitItem = false;
8887
ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8888
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
8889
EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8890
window->DC.LayoutType = ImGuiLayoutType_Vertical;
8891
window->DC.IsSameLine = false;
8892
window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8893
window->DC.MenuBarAppending = false;
8894
window->DC.CursorMaxPos = restore_cursor_max_pos;
8895
}
8896
8897
// Important: calling order matters!
8898
// FIXME: Somehow overlapping with docking tech.
8899
// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8900
bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8901
{
8902
IM_ASSERT(dir != ImGuiDir_None);
8903
8904
ImGuiWindow* bar_window = FindWindowByName(name);
8905
if (bar_window == NULL || bar_window->BeginCount == 0)
8906
{
8907
// Calculate and set window size/position
8908
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8909
ImRect avail_rect = viewport->GetBuildWorkRect();
8910
ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8911
ImVec2 pos = avail_rect.Min;
8912
if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8913
pos[axis] = avail_rect.Max[axis] - axis_size;
8914
ImVec2 size = avail_rect.GetSize();
8915
size[axis] = axis_size;
8916
SetNextWindowPos(pos);
8917
SetNextWindowSize(size);
8918
8919
// Report our size into work area (for next frame) using actual window size
8920
if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8921
viewport->BuildWorkInsetMin[axis] += axis_size;
8922
else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8923
viewport->BuildWorkInsetMax[axis] += axis_size;
8924
}
8925
8926
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
8927
PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
8928
PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
8929
bool is_open = Begin(name, NULL, window_flags);
8930
PopStyleVar(2);
8931
8932
return is_open;
8933
}
8934
8935
bool ImGui::BeginMainMenuBar()
8936
{
8937
ImGuiContext& g = *GImGui;
8938
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8939
8940
// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8941
// FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8942
// FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8943
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
8944
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8945
float height = GetFrameHeight();
8946
bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
8947
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8948
if (!is_open)
8949
{
8950
End();
8951
return false;
8952
}
8953
8954
// Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
8955
g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
8956
BeginMenuBar();
8957
return is_open;
8958
}
8959
8960
void ImGui::EndMainMenuBar()
8961
{
8962
ImGuiContext& g = *GImGui;
8963
if (!g.CurrentWindow->DC.MenuBarAppending)
8964
{
8965
IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
8966
return;
8967
}
8968
8969
EndMenuBar();
8970
g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
8971
8972
// When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8973
// FIXME: With this strategy we won't be able to restore a NULL focus.
8974
if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
8975
FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8976
8977
End();
8978
}
8979
8980
static bool IsRootOfOpenMenuSet()
8981
{
8982
ImGuiContext& g = *GImGui;
8983
ImGuiWindow* window = g.CurrentWindow;
8984
if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
8985
return false;
8986
8987
// Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
8988
// (e.g. inside menu bar vs loose menu items) based on parent ID.
8989
// This would however prevent the use of e.g. PushID() user code submitting menus.
8990
// Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
8991
// making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
8992
// Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
8993
// doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
8994
// 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.
8995
// 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
8996
// 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
8997
// it likely won't be a problem anyone runs into.
8998
const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
8999
if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
9000
return false;
9001
return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true);
9002
}
9003
9004
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
9005
{
9006
ImGuiWindow* window = GetCurrentWindow();
9007
if (window->SkipItems)
9008
return false;
9009
9010
ImGuiContext& g = *GImGui;
9011
const ImGuiStyle& style = g.Style;
9012
const ImGuiID id = window->GetID(label);
9013
bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
9014
9015
// 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)
9016
// 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.
9017
ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
9018
if (window->Flags & ImGuiWindowFlags_ChildMenu)
9019
window_flags |= ImGuiWindowFlags_ChildWindow;
9020
9021
// If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
9022
// 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.
9023
// If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
9024
if (g.MenusIdSubmittedThisFrame.contains(id))
9025
{
9026
if (menu_is_open)
9027
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)
9028
else
9029
g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
9030
return menu_is_open;
9031
}
9032
9033
// Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
9034
g.MenusIdSubmittedThisFrame.push_back(id);
9035
9036
ImVec2 label_size = CalcTextSize(label, NULL, true);
9037
9038
// 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)
9039
// This is only done for items for the menu set and not the full parent window.
9040
const bool menuset_is_open = IsRootOfOpenMenuSet();
9041
if (menuset_is_open)
9042
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9043
9044
// The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
9045
// However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
9046
// e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
9047
ImVec2 popup_pos, pos = window->DC.CursorPos;
9048
PushID(label);
9049
if (!enabled)
9050
BeginDisabled();
9051
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9052
bool pressed;
9053
9054
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9055
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
9056
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9057
{
9058
// Menu inside a horizontal menu bar
9059
// Selectable extend their highlight by half ItemSpacing in each direction.
9060
// For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
9061
popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
9062
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9063
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9064
float w = label_size.x;
9065
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9066
pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));
9067
LogSetNextTextDecoration("[", "]");
9068
RenderText(text_pos, label);
9069
PopStyleVar();
9070
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().
9071
}
9072
else
9073
{
9074
// Menu inside a regular/vertical menu
9075
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9076
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9077
popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
9078
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9079
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9080
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
9081
float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9082
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9083
pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9084
LogSetNextTextDecoration("", ">");
9085
RenderText(text_pos, label);
9086
if (icon_w > 0.0f)
9087
RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9088
RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
9089
}
9090
if (!enabled)
9091
EndDisabled();
9092
9093
const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
9094
if (menuset_is_open)
9095
PopItemFlag();
9096
9097
bool want_open = false;
9098
bool want_open_nav_init = false;
9099
bool want_close = false;
9100
if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
9101
{
9102
// Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
9103
// Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
9104
bool moving_toward_child_menu = false;
9105
ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
9106
ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
9107
if (g.HoveredWindow == window && child_menu_window != NULL)
9108
{
9109
const float ref_unit = g.FontSize; // FIXME-DPI
9110
const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
9111
const ImRect next_window_rect = child_menu_window->Rect();
9112
ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
9113
ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
9114
ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
9115
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.
9116
ta.x += child_dir * -0.5f;
9117
tb.x += child_dir * ref_unit;
9118
tc.x += child_dir * ref_unit;
9119
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
9120
tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
9121
moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
9122
//GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
9123
}
9124
9125
// 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)
9126
// 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.
9127
// (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.)
9128
if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
9129
want_close = true;
9130
9131
// Open
9132
// (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
9133
if (!menu_is_open && pressed) // Click/activate to open
9134
want_open = true;
9135
else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
9136
want_open = true;
9137
else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9138
want_open = true;
9139
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9140
{
9141
want_open = want_open_nav_init = true;
9142
NavMoveRequestCancel();
9143
SetNavCursorVisibleAfterMove();
9144
}
9145
}
9146
else
9147
{
9148
// Menu bar
9149
if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9150
{
9151
want_close = true;
9152
want_open = menu_is_open = false;
9153
}
9154
else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9155
{
9156
want_open = true;
9157
}
9158
else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9159
{
9160
want_open = true;
9161
NavMoveRequestCancel();
9162
}
9163
}
9164
9165
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.. }'
9166
want_close = true;
9167
if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
9168
ClosePopupToLevel(g.BeginPopupStack.Size, true);
9169
9170
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9171
PopID();
9172
9173
if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9174
{
9175
// 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.
9176
OpenPopup(label);
9177
}
9178
else if (want_open)
9179
{
9180
menu_is_open = true;
9181
OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9182
}
9183
9184
if (menu_is_open)
9185
{
9186
ImGuiLastItemData last_item_in_parent = g.LastItemData;
9187
SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9188
PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9189
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)
9190
PopStyleVar();
9191
if (menu_is_open)
9192
{
9193
// Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9194
// Perform an init request in the case the popup was already open (via a previous mouse hover)
9195
if (want_open && want_open_nav_init && !g.NavInitRequest)
9196
{
9197
FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
9198
NavInitWindow(g.CurrentWindow, false);
9199
}
9200
9201
// Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9202
// (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9203
g.LastItemData = last_item_in_parent;
9204
if (g.HoveredWindow == window)
9205
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9206
}
9207
}
9208
else
9209
{
9210
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9211
}
9212
9213
return menu_is_open;
9214
}
9215
9216
bool ImGui::BeginMenu(const char* label, bool enabled)
9217
{
9218
return BeginMenuEx(label, NULL, enabled);
9219
}
9220
9221
void ImGui::EndMenu()
9222
{
9223
// Nav: When a left move request our menu failed, close ourselves.
9224
ImGuiContext& g = *GImGui;
9225
ImGuiWindow* window = g.CurrentWindow;
9226
IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
9227
ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9228
if (window->BeginCount == window->BeginCountPreviousFrame)
9229
if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9230
if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9231
{
9232
ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9233
NavMoveRequestCancel();
9234
}
9235
9236
EndPopup();
9237
}
9238
9239
bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9240
{
9241
ImGuiWindow* window = GetCurrentWindow();
9242
if (window->SkipItems)
9243
return false;
9244
9245
ImGuiContext& g = *GImGui;
9246
ImGuiStyle& style = g.Style;
9247
ImVec2 pos = window->DC.CursorPos;
9248
ImVec2 label_size = CalcTextSize(label, NULL, true);
9249
9250
// See BeginMenuEx() for comments about this.
9251
const bool menuset_is_open = IsRootOfOpenMenuSet();
9252
if (menuset_is_open)
9253
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9254
9255
// We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9256
// 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.
9257
bool pressed;
9258
PushID(label);
9259
if (!enabled)
9260
BeginDisabled();
9261
9262
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9263
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9264
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9265
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9266
{
9267
// Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9268
// Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9269
float w = label_size.x;
9270
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9271
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9272
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9273
pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f));
9274
PopStyleVar();
9275
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9276
RenderText(text_pos, label);
9277
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().
9278
}
9279
else
9280
{
9281
// Menu item inside a vertical menu
9282
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9283
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9284
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9285
float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9286
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9287
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9288
float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9289
pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9290
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9291
{
9292
RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label);
9293
if (icon_w > 0.0f)
9294
RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9295
if (shortcut_w > 0.0f)
9296
{
9297
PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9298
LogSetNextTextDecoration("(", ")");
9299
RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9300
PopStyleColor();
9301
}
9302
if (selected)
9303
RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9304
}
9305
}
9306
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9307
if (!enabled)
9308
EndDisabled();
9309
PopID();
9310
if (menuset_is_open)
9311
PopItemFlag();
9312
9313
return pressed;
9314
}
9315
9316
bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9317
{
9318
return MenuItemEx(label, NULL, shortcut, selected, enabled);
9319
}
9320
9321
bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9322
{
9323
if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9324
{
9325
if (p_selected)
9326
*p_selected = !*p_selected;
9327
return true;
9328
}
9329
return false;
9330
}
9331
9332
//-------------------------------------------------------------------------
9333
// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9334
//-------------------------------------------------------------------------
9335
// - BeginTabBar()
9336
// - BeginTabBarEx() [Internal]
9337
// - EndTabBar()
9338
// - TabBarLayout() [Internal]
9339
// - TabBarCalcTabID() [Internal]
9340
// - TabBarCalcMaxTabWidth() [Internal]
9341
// - TabBarFindTabById() [Internal]
9342
// - TabBarFindTabByOrder() [Internal]
9343
// - TabBarGetCurrentTab() [Internal]
9344
// - TabBarGetTabName() [Internal]
9345
// - TabBarRemoveTab() [Internal]
9346
// - TabBarCloseTab() [Internal]
9347
// - TabBarScrollClamp() [Internal]
9348
// - TabBarScrollToTab() [Internal]
9349
// - TabBarQueueFocus() [Internal]
9350
// - TabBarQueueReorder() [Internal]
9351
// - TabBarProcessReorderFromMousePos() [Internal]
9352
// - TabBarProcessReorder() [Internal]
9353
// - TabBarScrollingButtons() [Internal]
9354
// - TabBarTabListPopupButton() [Internal]
9355
//-------------------------------------------------------------------------
9356
9357
struct ImGuiTabBarSection
9358
{
9359
int TabCount; // Number of tabs in this section.
9360
float Width; // Sum of width of tabs in this section (after shrinking down)
9361
float Spacing; // Horizontal spacing at the end of the section.
9362
9363
ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
9364
};
9365
9366
namespace ImGui
9367
{
9368
static void TabBarLayout(ImGuiTabBar* tab_bar);
9369
static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9370
static float TabBarCalcMaxTabWidth();
9371
static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9372
static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9373
static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9374
static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9375
}
9376
9377
ImGuiTabBar::ImGuiTabBar()
9378
{
9379
memset(this, 0, sizeof(*this));
9380
CurrFrameVisible = PrevFrameVisible = -1;
9381
LastTabItemIdx = -1;
9382
}
9383
9384
static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9385
{
9386
return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9387
}
9388
9389
static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9390
{
9391
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9392
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9393
const int a_section = TabItemGetSectionIdx(a);
9394
const int b_section = TabItemGetSectionIdx(b);
9395
if (a_section != b_section)
9396
return a_section - b_section;
9397
return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9398
}
9399
9400
static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9401
{
9402
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9403
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9404
return (int)(a->BeginOrder - b->BeginOrder);
9405
}
9406
9407
static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9408
{
9409
ImGuiContext& g = *GImGui;
9410
return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9411
}
9412
9413
static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9414
{
9415
ImGuiContext& g = *GImGui;
9416
if (g.TabBars.Contains(tab_bar))
9417
return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9418
return ImGuiPtrOrIndex(tab_bar);
9419
}
9420
9421
bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9422
{
9423
ImGuiContext& g = *GImGui;
9424
ImGuiWindow* window = g.CurrentWindow;
9425
if (window->SkipItems)
9426
return false;
9427
9428
ImGuiID id = window->GetID(str_id);
9429
ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9430
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);
9431
tab_bar->ID = id;
9432
tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9433
tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9434
//if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9435
flags |= ImGuiTabBarFlags_IsFocused;
9436
return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9437
}
9438
9439
bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9440
{
9441
ImGuiContext& g = *GImGui;
9442
ImGuiWindow* window = g.CurrentWindow;
9443
if (window->SkipItems)
9444
return false;
9445
9446
IM_ASSERT(tab_bar->ID != 0);
9447
if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9448
PushOverrideID(tab_bar->ID);
9449
9450
// Add to stack
9451
g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9452
g.CurrentTabBar = tab_bar;
9453
tab_bar->Window = window;
9454
9455
// Append with multiple BeginTabBar()/EndTabBar() pairs.
9456
tab_bar->BackupCursorPos = window->DC.CursorPos;
9457
if (tab_bar->CurrFrameVisible == g.FrameCount)
9458
{
9459
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9460
tab_bar->BeginCount++;
9461
return true;
9462
}
9463
9464
// Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9465
if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9466
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9467
tab_bar->TabsAddedNew = false;
9468
9469
// Flags
9470
if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9471
flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9472
9473
tab_bar->Flags = flags;
9474
tab_bar->BarRect = tab_bar_bb;
9475
tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9476
tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9477
tab_bar->CurrFrameVisible = g.FrameCount;
9478
tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9479
tab_bar->CurrTabsContentsHeight = 0.0f;
9480
tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9481
tab_bar->FramePadding = g.Style.FramePadding;
9482
tab_bar->TabsActiveCount = 0;
9483
tab_bar->LastTabItemIdx = -1;
9484
tab_bar->BeginCount = 1;
9485
9486
// Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9487
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9488
9489
// Draw separator
9490
// (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9491
const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9492
if (g.Style.TabBarBorderSize > 0.0f)
9493
{
9494
const float y = tab_bar->BarRect.Max.y;
9495
window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9496
}
9497
return true;
9498
}
9499
9500
void ImGui::EndTabBar()
9501
{
9502
ImGuiContext& g = *GImGui;
9503
ImGuiWindow* window = g.CurrentWindow;
9504
if (window->SkipItems)
9505
return;
9506
9507
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9508
if (tab_bar == NULL)
9509
{
9510
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9511
return;
9512
}
9513
9514
// Fallback in case no TabItem have been submitted
9515
if (tab_bar->WantLayout)
9516
TabBarLayout(tab_bar);
9517
9518
// Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9519
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9520
if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9521
{
9522
tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9523
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9524
}
9525
else
9526
{
9527
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9528
}
9529
if (tab_bar->BeginCount > 1)
9530
window->DC.CursorPos = tab_bar->BackupCursorPos;
9531
9532
tab_bar->LastTabItemIdx = -1;
9533
if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9534
PopID();
9535
9536
g.CurrentTabBarStack.pop_back();
9537
g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9538
}
9539
9540
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9541
static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9542
{
9543
return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9544
}
9545
9546
// This is called only once a frame before by the first call to ItemTab()
9547
// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9548
static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9549
{
9550
ImGuiContext& g = *GImGui;
9551
tab_bar->WantLayout = false;
9552
9553
// Garbage collect by compacting list
9554
// Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9555
int tab_dst_n = 0;
9556
bool need_sort_by_section = false;
9557
ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9558
for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9559
{
9560
ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9561
if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9562
{
9563
// Remove tab
9564
if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9565
if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9566
if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9567
continue;
9568
}
9569
if (tab_dst_n != tab_src_n)
9570
tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9571
9572
tab = &tab_bar->Tabs[tab_dst_n];
9573
tab->IndexDuringLayout = (ImS16)tab_dst_n;
9574
9575
// We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9576
int curr_tab_section_n = TabItemGetSectionIdx(tab);
9577
if (tab_dst_n > 0)
9578
{
9579
ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9580
int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
9581
if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9582
need_sort_by_section = true;
9583
if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9584
need_sort_by_section = true;
9585
}
9586
9587
sections[curr_tab_section_n].TabCount++;
9588
tab_dst_n++;
9589
}
9590
if (tab_bar->Tabs.Size != tab_dst_n)
9591
tab_bar->Tabs.resize(tab_dst_n);
9592
9593
if (need_sort_by_section)
9594
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
9595
9596
// Calculate spacing between sections
9597
sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9598
sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9599
9600
// Setup next selected tab
9601
ImGuiID scroll_to_tab_id = 0;
9602
if (tab_bar->NextSelectedTabId)
9603
{
9604
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9605
tab_bar->NextSelectedTabId = 0;
9606
scroll_to_tab_id = tab_bar->SelectedTabId;
9607
}
9608
9609
// Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9610
if (tab_bar->ReorderRequestTabId != 0)
9611
{
9612
if (TabBarProcessReorder(tab_bar))
9613
if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9614
scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9615
tab_bar->ReorderRequestTabId = 0;
9616
}
9617
9618
// Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9619
const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9620
if (tab_list_popup_button)
9621
if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9622
scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9623
9624
// Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9625
// (whereas our tabs are stored as: leading, central, trailing)
9626
int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9627
g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
9628
9629
// Compute ideal tabs widths + store them into shrink buffer
9630
ImGuiTabItem* most_recently_selected_tab = NULL;
9631
int curr_section_n = -1;
9632
bool found_selected_tab_id = false;
9633
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9634
{
9635
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9636
IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9637
9638
if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9639
most_recently_selected_tab = tab;
9640
if (tab->ID == tab_bar->SelectedTabId)
9641
found_selected_tab_id = true;
9642
if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9643
scroll_to_tab_id = tab->ID;
9644
9645
// Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9646
// Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9647
// and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9648
const char* tab_name = TabBarGetTabName(tab_bar, tab);
9649
const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9650
tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
9651
9652
int section_n = TabItemGetSectionIdx(tab);
9653
ImGuiTabBarSection* section = &sections[section_n];
9654
section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9655
curr_section_n = section_n;
9656
9657
// Store data so we can build an array sorted by width if we need to shrink tabs down
9658
IM_MSVC_WARNING_SUPPRESS(6385);
9659
ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9660
shrink_width_item->Index = tab_n;
9661
shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9662
tab->Width = ImMax(tab->ContentWidth, 1.0f);
9663
}
9664
9665
// Compute total ideal width (used for e.g. auto-resizing a window)
9666
tab_bar->WidthAllTabsIdeal = 0.0f;
9667
for (int section_n = 0; section_n < 3; section_n++)
9668
tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9669
9670
// Horizontal scrolling buttons
9671
// (note that TabBarScrollButtons() will alter BarRect.Max.x)
9672
if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
9673
if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9674
{
9675
scroll_to_tab_id = scroll_and_select_tab->ID;
9676
if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9677
tab_bar->SelectedTabId = scroll_to_tab_id;
9678
}
9679
9680
// Shrink widths if full tabs don't fit in their allocated space
9681
float section_0_w = sections[0].Width + sections[0].Spacing;
9682
float section_1_w = sections[1].Width + sections[1].Spacing;
9683
float section_2_w = sections[2].Width + sections[2].Spacing;
9684
bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9685
float width_excess;
9686
if (central_section_is_visible)
9687
width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
9688
else
9689
width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9690
9691
// With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9692
if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
9693
{
9694
int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9695
int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9696
ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess);
9697
9698
// Apply shrunk values into tabs and sections
9699
for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9700
{
9701
ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9702
float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9703
if (shrinked_width < 0.0f)
9704
continue;
9705
9706
shrinked_width = ImMax(1.0f, shrinked_width);
9707
int section_n = TabItemGetSectionIdx(tab);
9708
sections[section_n].Width -= (tab->Width - shrinked_width);
9709
tab->Width = shrinked_width;
9710
}
9711
}
9712
9713
// Layout all active tabs
9714
int section_tab_index = 0;
9715
float tab_offset = 0.0f;
9716
tab_bar->WidthAllTabs = 0.0f;
9717
for (int section_n = 0; section_n < 3; section_n++)
9718
{
9719
ImGuiTabBarSection* section = &sections[section_n];
9720
if (section_n == 2)
9721
tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
9722
9723
for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9724
{
9725
ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9726
tab->Offset = tab_offset;
9727
tab->NameOffset = -1;
9728
tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9729
}
9730
tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
9731
tab_offset += section->Spacing;
9732
section_tab_index += section->TabCount;
9733
}
9734
9735
// Clear name buffers
9736
tab_bar->TabsNames.Buf.resize(0);
9737
9738
// If we have lost the selected tab, select the next most recently active one
9739
if (found_selected_tab_id == false)
9740
tab_bar->SelectedTabId = 0;
9741
if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9742
scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9743
9744
// Lock in visible tab
9745
tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9746
tab_bar->VisibleTabWasSubmitted = false;
9747
9748
// Apply request requests
9749
if (scroll_to_tab_id != 0)
9750
TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
9751
else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
9752
{
9753
const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9754
const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9755
if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
9756
{
9757
const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9758
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9759
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
9760
}
9761
SetKeyOwner(wheel_key, tab_bar->ID);
9762
}
9763
9764
// Update scrolling
9765
tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
9766
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
9767
if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9768
{
9769
// Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9770
// Teleport if we are aiming far off the visible line
9771
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
9772
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9773
const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9774
tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9775
}
9776
else
9777
{
9778
tab_bar->ScrollingSpeed = 0.0f;
9779
}
9780
tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9781
tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9782
9783
// Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9784
ImGuiWindow* window = g.CurrentWindow;
9785
window->DC.CursorPos = tab_bar->BarRect.Min;
9786
ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
9787
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9788
}
9789
9790
// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9791
static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9792
{
9793
IM_ASSERT(docked_window == NULL); // master branch only
9794
IM_UNUSED(docked_window);
9795
if (tab_bar->Flags & ImGuiTabBarFlags_DockNode)
9796
{
9797
ImGuiID id = ImHashStr(label);
9798
KeepAliveID(id);
9799
return id;
9800
}
9801
else
9802
{
9803
ImGuiWindow* window = GImGui->CurrentWindow;
9804
return window->GetID(label);
9805
}
9806
}
9807
9808
static float ImGui::TabBarCalcMaxTabWidth()
9809
{
9810
ImGuiContext& g = *GImGui;
9811
return g.FontSize * 20.0f;
9812
}
9813
9814
ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9815
{
9816
if (tab_id != 0)
9817
for (int n = 0; n < tab_bar->Tabs.Size; n++)
9818
if (tab_bar->Tabs[n].ID == tab_id)
9819
return &tab_bar->Tabs[n];
9820
return NULL;
9821
}
9822
9823
// Order = visible order, not submission order! (which is tab->BeginOrder)
9824
ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9825
{
9826
if (order < 0 || order >= tab_bar->Tabs.Size)
9827
return NULL;
9828
return &tab_bar->Tabs[order];
9829
}
9830
9831
ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9832
{
9833
if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9834
return NULL;
9835
return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9836
}
9837
9838
const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9839
{
9840
if (tab->NameOffset == -1)
9841
return "N/A";
9842
IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9843
return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9844
}
9845
9846
// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9847
void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9848
{
9849
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9850
tab_bar->Tabs.erase(tab);
9851
if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9852
if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9853
if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9854
}
9855
9856
// Called on manual closure attempt
9857
void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9858
{
9859
if (tab->Flags & ImGuiTabItemFlags_Button)
9860
return; // A button appended with TabItemButton().
9861
9862
if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9863
{
9864
// This will remove a frame of lag for selecting another tab on closure.
9865
// 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
9866
tab->WantClose = true;
9867
if (tab_bar->VisibleTabId == tab->ID)
9868
{
9869
tab->LastFrameVisible = -1;
9870
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9871
}
9872
}
9873
else
9874
{
9875
// Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9876
if (tab_bar->VisibleTabId != tab->ID)
9877
TabBarQueueFocus(tab_bar, tab);
9878
}
9879
}
9880
9881
static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9882
{
9883
scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9884
return ImMax(scrolling, 0.0f);
9885
}
9886
9887
// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9888
static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9889
{
9890
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9891
if (tab == NULL)
9892
return;
9893
if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9894
return;
9895
9896
ImGuiContext& g = *GImGui;
9897
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)
9898
int order = TabBarGetTabOrder(tab_bar, tab);
9899
9900
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9901
float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9902
9903
// We make all tabs positions all relative Sections[0].Width to make code simpler
9904
float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9905
float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9906
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9907
if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9908
{
9909
// Scroll to the left
9910
tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
9911
tab_bar->ScrollingTarget = tab_x1;
9912
}
9913
else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9914
{
9915
// Scroll to the right
9916
tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
9917
tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9918
}
9919
}
9920
9921
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9922
{
9923
tab_bar->NextSelectedTabId = tab->ID;
9924
}
9925
9926
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
9927
{
9928
IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
9929
ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
9930
tab_bar->NextSelectedTabId = tab_id;
9931
}
9932
9933
void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
9934
{
9935
IM_ASSERT(offset != 0);
9936
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9937
tab_bar->ReorderRequestTabId = tab->ID;
9938
tab_bar->ReorderRequestOffset = (ImS16)offset;
9939
}
9940
9941
void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
9942
{
9943
ImGuiContext& g = *GImGui;
9944
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9945
if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
9946
return;
9947
9948
const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9949
const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
9950
9951
// Count number of contiguous tabs we are crossing over
9952
const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
9953
const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
9954
int dst_idx = src_idx;
9955
for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
9956
{
9957
// Reordered tabs must share the same section
9958
const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
9959
if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
9960
break;
9961
if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
9962
break;
9963
dst_idx = i;
9964
9965
// Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
9966
const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
9967
const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
9968
//GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
9969
if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
9970
break;
9971
}
9972
9973
if (dst_idx != src_idx)
9974
TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
9975
}
9976
9977
bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
9978
{
9979
ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
9980
if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
9981
return false;
9982
9983
//IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
9984
int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
9985
if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
9986
return false;
9987
9988
// Reordered tabs must share the same section
9989
// (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
9990
ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
9991
if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
9992
return false;
9993
if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
9994
return false;
9995
9996
ImGuiTabItem item_tmp = *tab1;
9997
ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
9998
ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
9999
const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
10000
memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
10001
*tab2 = item_tmp;
10002
10003
if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
10004
MarkIniSettingsDirty();
10005
return true;
10006
}
10007
10008
static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
10009
{
10010
ImGuiContext& g = *GImGui;
10011
ImGuiWindow* window = g.CurrentWindow;
10012
10013
const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
10014
const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
10015
10016
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10017
//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));
10018
10019
int select_dir = 0;
10020
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10021
arrow_col.w *= 0.5f;
10022
10023
PushStyleColor(ImGuiCol_Text, arrow_col);
10024
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10025
PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
10026
const float backup_repeat_delay = g.IO.KeyRepeatDelay;
10027
const float backup_repeat_rate = g.IO.KeyRepeatRate;
10028
g.IO.KeyRepeatDelay = 0.250f;
10029
g.IO.KeyRepeatRate = 0.200f;
10030
float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
10031
window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
10032
if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10033
select_dir = -1;
10034
window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
10035
if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10036
select_dir = +1;
10037
PopItemFlag();
10038
PopStyleColor(2);
10039
g.IO.KeyRepeatRate = backup_repeat_rate;
10040
g.IO.KeyRepeatDelay = backup_repeat_delay;
10041
10042
ImGuiTabItem* tab_to_scroll_to = NULL;
10043
if (select_dir != 0)
10044
if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
10045
{
10046
int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
10047
int target_order = selected_order + select_dir;
10048
10049
// Skip tab item buttons until another tab item is found or end is reached
10050
while (tab_to_scroll_to == NULL)
10051
{
10052
// If we are at the end of the list, still scroll to make our tab visible
10053
tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
10054
10055
// Cross through buttons
10056
// (even if first/last item is a button, return it so we can update the scroll)
10057
if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
10058
{
10059
target_order += select_dir;
10060
selected_order += select_dir;
10061
tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
10062
}
10063
}
10064
}
10065
window->DC.CursorPos = backup_cursor_pos;
10066
tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
10067
10068
return tab_to_scroll_to;
10069
}
10070
10071
static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
10072
{
10073
ImGuiContext& g = *GImGui;
10074
ImGuiWindow* window = g.CurrentWindow;
10075
10076
// We use g.Style.FramePadding.y to match the square ArrowButton size
10077
const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
10078
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10079
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
10080
tab_bar->BarRect.Min.x += tab_list_popup_button_width;
10081
10082
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10083
arrow_col.w *= 0.5f;
10084
PushStyleColor(ImGuiCol_Text, arrow_col);
10085
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10086
bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
10087
PopStyleColor(2);
10088
10089
ImGuiTabItem* tab_to_select = NULL;
10090
if (open)
10091
{
10092
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10093
{
10094
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10095
if (tab->Flags & ImGuiTabItemFlags_Button)
10096
continue;
10097
10098
const char* tab_name = TabBarGetTabName(tab_bar, tab);
10099
if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
10100
tab_to_select = tab;
10101
}
10102
EndCombo();
10103
}
10104
10105
window->DC.CursorPos = backup_cursor_pos;
10106
return tab_to_select;
10107
}
10108
10109
//-------------------------------------------------------------------------
10110
// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10111
//-------------------------------------------------------------------------
10112
// - BeginTabItem()
10113
// - EndTabItem()
10114
// - TabItemButton()
10115
// - TabItemEx() [Internal]
10116
// - SetTabItemClosed()
10117
// - TabItemCalcSize() [Internal]
10118
// - TabItemBackground() [Internal]
10119
// - TabItemLabelAndCloseButton() [Internal]
10120
//-------------------------------------------------------------------------
10121
10122
bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10123
{
10124
ImGuiContext& g = *GImGui;
10125
ImGuiWindow* window = g.CurrentWindow;
10126
if (window->SkipItems)
10127
return false;
10128
10129
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10130
if (tab_bar == NULL)
10131
{
10132
IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
10133
return false;
10134
}
10135
IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10136
10137
bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10138
if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10139
{
10140
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10141
PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10142
}
10143
return ret;
10144
}
10145
10146
void ImGui::EndTabItem()
10147
{
10148
ImGuiContext& g = *GImGui;
10149
ImGuiWindow* window = g.CurrentWindow;
10150
if (window->SkipItems)
10151
return;
10152
10153
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10154
if (tab_bar == NULL)
10155
{
10156
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10157
return;
10158
}
10159
IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10160
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10161
if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10162
PopID();
10163
}
10164
10165
bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10166
{
10167
ImGuiContext& g = *GImGui;
10168
ImGuiWindow* window = g.CurrentWindow;
10169
if (window->SkipItems)
10170
return false;
10171
10172
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10173
if (tab_bar == NULL)
10174
{
10175
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10176
return false;
10177
}
10178
return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10179
}
10180
10181
void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10182
{
10183
ImGuiContext& g = *GImGui;
10184
ImGuiWindow* window = g.CurrentWindow;
10185
if (window->SkipItems)
10186
return;
10187
10188
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10189
if (tab_bar == NULL)
10190
{
10191
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10192
return;
10193
}
10194
SetNextItemWidth(width);
10195
TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10196
}
10197
10198
bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10199
{
10200
// Layout whole tab bar if not already done
10201
ImGuiContext& g = *GImGui;
10202
if (tab_bar->WantLayout)
10203
{
10204
ImGuiNextItemData backup_next_item_data = g.NextItemData;
10205
TabBarLayout(tab_bar);
10206
g.NextItemData = backup_next_item_data;
10207
}
10208
ImGuiWindow* window = g.CurrentWindow;
10209
if (window->SkipItems)
10210
return false;
10211
10212
const ImGuiStyle& style = g.Style;
10213
const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10214
10215
// If the user called us with *p_open == false, we early out and don't render.
10216
// 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.
10217
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10218
if (p_open && !*p_open)
10219
{
10220
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10221
return false;
10222
}
10223
10224
IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10225
IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10226
10227
// Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10228
if (flags & ImGuiTabItemFlags_NoCloseButton)
10229
p_open = NULL;
10230
else if (p_open == NULL)
10231
flags |= ImGuiTabItemFlags_NoCloseButton;
10232
10233
// Acquire tab data
10234
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10235
bool tab_is_new = false;
10236
if (tab == NULL)
10237
{
10238
tab_bar->Tabs.push_back(ImGuiTabItem());
10239
tab = &tab_bar->Tabs.back();
10240
tab->ID = id;
10241
tab_bar->TabsAddedNew = tab_is_new = true;
10242
}
10243
tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10244
10245
// Calculate tab contents size
10246
ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10247
tab->RequestedWidth = -1.0f;
10248
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10249
size.x = tab->RequestedWidth = g.NextItemData.Width;
10250
if (tab_is_new)
10251
tab->Width = ImMax(1.0f, size.x);
10252
tab->ContentWidth = size.x;
10253
tab->BeginOrder = tab_bar->TabsActiveCount++;
10254
10255
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10256
const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10257
const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10258
const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10259
const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10260
tab->LastFrameVisible = g.FrameCount;
10261
tab->Flags = flags;
10262
10263
// Append name _WITH_ the zero-terminator
10264
if (docked_window != NULL)
10265
{
10266
IM_ASSERT(docked_window == NULL); // master branch only
10267
}
10268
else
10269
{
10270
tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10271
tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1);
10272
}
10273
10274
// Update selected tab
10275
if (!is_tab_button)
10276
{
10277
if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10278
if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10279
TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10280
if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10281
TabBarQueueFocus(tab_bar, tab);
10282
}
10283
10284
// Lock visibility
10285
// (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
10286
bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10287
if (tab_contents_visible)
10288
tab_bar->VisibleTabWasSubmitted = true;
10289
10290
// On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10291
if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing)
10292
if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10293
tab_contents_visible = true;
10294
10295
// Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10296
// and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10297
if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10298
{
10299
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10300
if (is_tab_button)
10301
return false;
10302
return tab_contents_visible;
10303
}
10304
10305
if (tab_bar->SelectedTabId == id)
10306
tab->LastFrameSelected = g.FrameCount;
10307
10308
// Backup current layout position
10309
const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10310
10311
// Layout
10312
const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10313
size.x = tab->Width;
10314
if (is_central_section)
10315
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10316
else
10317
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10318
ImVec2 pos = window->DC.CursorPos;
10319
ImRect bb(pos, pos + size);
10320
10321
// 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)
10322
const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10323
if (want_clip_rect)
10324
PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10325
10326
ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10327
ItemSize(bb.GetSize(), style.FramePadding.y);
10328
window->DC.CursorMaxPos = backup_cursor_max_pos;
10329
10330
if (!ItemAdd(bb, id))
10331
{
10332
if (want_clip_rect)
10333
PopClipRect();
10334
window->DC.CursorPos = backup_main_cursor_pos;
10335
return tab_contents_visible;
10336
}
10337
10338
// Click to Select a tab
10339
// Allow the close button to overlap
10340
ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10341
if (g.DragDropActive)
10342
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10343
bool hovered, held, pressed;
10344
if (flags & ImGuiTabItemFlags_Invisible)
10345
hovered = held = pressed = false;
10346
else
10347
pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10348
if (pressed && !is_tab_button)
10349
TabBarQueueFocus(tab_bar, tab);
10350
10351
// Drag and drop: re-order tabs
10352
if (held && !tab_appearing && IsMouseDragging(0))
10353
{
10354
if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable))
10355
{
10356
// While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10357
if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10358
{
10359
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10360
}
10361
else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10362
{
10363
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10364
}
10365
}
10366
}
10367
10368
#if 0
10369
if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10370
{
10371
// Enlarge tab display when hovering
10372
bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10373
display_draw_list = GetForegroundDrawList(window);
10374
TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10375
}
10376
#endif
10377
10378
// Render tab shape
10379
const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10380
if (is_visible)
10381
{
10382
ImDrawList* display_draw_list = window->DrawList;
10383
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));
10384
TabItemBackground(display_draw_list, bb, flags, tab_col);
10385
if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10386
{
10387
// Might be moved to TabItemBackground() ?
10388
ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10389
ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10390
ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10391
if (style.TabRounding > 0.0f)
10392
{
10393
float rounding = style.TabRounding;
10394
display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9);
10395
display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11);
10396
display_draw_list->PathStroke(overline_col, 0, style.TabBarOverlineSize);
10397
}
10398
else
10399
{
10400
display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize);
10401
}
10402
}
10403
RenderNavCursor(bb, id);
10404
10405
// Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10406
const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10407
if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10408
TabBarQueueFocus(tab_bar, tab);
10409
10410
if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10411
flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10412
10413
// Render tab label, process close button
10414
const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0;
10415
bool just_closed;
10416
bool text_clipped;
10417
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);
10418
if (just_closed && p_open != NULL)
10419
{
10420
*p_open = false;
10421
TabBarCloseTab(tab_bar, tab);
10422
}
10423
10424
// Tooltip
10425
// (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10426
// (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10427
// FIXME: This is a mess.
10428
// FIXME: We may want disabled tab to still display the tooltip?
10429
if (text_clipped && g.HoveredId == id && !held)
10430
if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10431
SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10432
}
10433
10434
// Restore main window position so user can draw there
10435
if (want_clip_rect)
10436
PopClipRect();
10437
window->DC.CursorPos = backup_main_cursor_pos;
10438
10439
IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10440
if (is_tab_button)
10441
return pressed;
10442
return tab_contents_visible;
10443
}
10444
10445
// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10446
// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10447
// Tabs closed by the close button will automatically be flagged to avoid this issue.
10448
void ImGui::SetTabItemClosed(const char* label)
10449
{
10450
ImGuiContext& g = *GImGui;
10451
bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10452
if (is_within_manual_tab_bar)
10453
{
10454
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10455
ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10456
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10457
tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10458
}
10459
}
10460
10461
ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10462
{
10463
ImGuiContext& g = *GImGui;
10464
ImVec2 label_size = CalcTextSize(label, NULL, true);
10465
ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10466
if (has_close_button_or_unsaved_marker)
10467
size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10468
else
10469
size.x += g.Style.FramePadding.x + 1.0f;
10470
return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10471
}
10472
10473
ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*)
10474
{
10475
IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch.
10476
return ImVec2(0.0f, 0.0f);
10477
}
10478
10479
void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10480
{
10481
// 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.
10482
ImGuiContext& g = *GImGui;
10483
const float width = bb.GetWidth();
10484
IM_UNUSED(flags);
10485
IM_ASSERT(width > 0.0f);
10486
const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
10487
const float y1 = bb.Min.y + 1.0f;
10488
const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10489
draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
10490
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9);
10491
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12);
10492
draw_list->PathLineTo(ImVec2(bb.Max.x, y2));
10493
draw_list->PathFillConvex(col);
10494
if (g.Style.TabBorderSize > 0.0f)
10495
{
10496
draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
10497
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
10498
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
10499
draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
10500
draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize);
10501
}
10502
}
10503
10504
// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10505
// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10506
void 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)
10507
{
10508
ImGuiContext& g = *GImGui;
10509
ImVec2 label_size = CalcTextSize(label, NULL, true);
10510
10511
if (out_just_closed)
10512
*out_just_closed = false;
10513
if (out_text_clipped)
10514
*out_text_clipped = false;
10515
10516
if (bb.GetWidth() <= 1.0f)
10517
return;
10518
10519
// In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10520
// But right now if you want to alter text color of tabs this is what you need to do.
10521
#if 0
10522
const float backup_alpha = g.Style.Alpha;
10523
if (!is_contents_visible)
10524
g.Style.Alpha *= 0.7f;
10525
#endif
10526
10527
// Render text label (with clipping + alpha gradient) + unsaved marker
10528
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);
10529
10530
// Return clipped state ignoring the close button
10531
if (out_text_clipped)
10532
{
10533
*out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
10534
//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));
10535
}
10536
10537
const float button_sz = g.FontSize;
10538
const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10539
10540
// Close Button & Unsaved Marker
10541
// We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10542
// 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10543
// 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10544
// 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10545
bool close_button_pressed = false;
10546
bool close_button_visible = false;
10547
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.
10548
10549
if (close_button_id != 0)
10550
{
10551
if (is_contents_visible)
10552
close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected));
10553
else
10554
close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected));
10555
}
10556
10557
// When tabs/document is unsaved, the unsaved marker takes priority over the close button.
10558
const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
10559
if (unsaved_marker_visible)
10560
{
10561
const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10562
RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text));
10563
}
10564
else if (close_button_visible)
10565
{
10566
ImGuiLastItemData last_item_backup = g.LastItemData;
10567
if (CloseButton(close_button_id, button_pos))
10568
close_button_pressed = true;
10569
g.LastItemData = last_item_backup;
10570
10571
// Close with middle mouse button
10572
if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
10573
close_button_pressed = true;
10574
}
10575
10576
// This is all rather complicated
10577
// (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10578
// 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..
10579
float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
10580
if (close_button_visible || unsaved_marker_visible)
10581
{
10582
const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
10583
if (visible_without_hover)
10584
{
10585
text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
10586
ellipsis_max_x -= button_sz * 0.90f;
10587
}
10588
else
10589
{
10590
text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
10591
}
10592
}
10593
LogSetNextTextDecoration("/", "\\");
10594
RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, NULL, &label_size);
10595
10596
#if 0
10597
if (!is_contents_visible)
10598
g.Style.Alpha = backup_alpha;
10599
#endif
10600
10601
if (out_just_closed)
10602
*out_just_closed = close_button_pressed;
10603
}
10604
10605
10606
#endif // #ifndef IMGUI_DISABLE
10607
10608