Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/dep/imgui/src/imgui_widgets.cpp
7367 views
1
// dear imgui, v1.92.6 WIP
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 ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining = NULL, ImVec2* out_offset = NULL, ImDrawTextFlags flags = 0);
139
140
//-------------------------------------------------------------------------
141
// [SECTION] Widgets: Text, etc.
142
//-------------------------------------------------------------------------
143
// - TextEx() [Internal]
144
// - TextUnformatted()
145
// - Text()
146
// - TextV()
147
// - TextColored()
148
// - TextColoredV()
149
// - TextDisabled()
150
// - TextDisabledV()
151
// - TextWrapped()
152
// - TextWrappedV()
153
// - LabelText()
154
// - LabelTextV()
155
// - BulletText()
156
// - BulletTextV()
157
//-------------------------------------------------------------------------
158
159
void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
160
{
161
ImGuiWindow* window = GetCurrentWindow();
162
if (window->SkipItems)
163
return;
164
ImGuiContext& g = *GImGui;
165
166
// Accept null ranges
167
if (text == text_end)
168
text = text_end = "";
169
170
// Calculate length
171
const char* text_begin = text;
172
if (text_end == NULL)
173
text_end = text + ImStrlen(text); // FIXME-OPT
174
175
const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
176
const float wrap_pos_x = window->DC.TextWrapPos;
177
const bool wrap_enabled = (wrap_pos_x >= 0.0f);
178
if (text_end - text <= 2000 || wrap_enabled)
179
{
180
// Common case
181
const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
182
const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
183
184
ImRect bb(text_pos, text_pos + text_size);
185
ItemSize(text_size, 0.0f);
186
if (!ItemAdd(bb, 0))
187
return;
188
189
// Render (we don't hide text after ## in this end-user function)
190
RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
191
}
192
else
193
{
194
// Long text!
195
// Perform manual coarse clipping to optimize for long multi-line text
196
// - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
197
// - 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.
198
// - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
199
const char* line = text;
200
const float line_height = GetTextLineHeight();
201
ImVec2 text_size(0, 0);
202
203
// Lines to skip (can't skip when logging text)
204
ImVec2 pos = text_pos;
205
if (!g.LogEnabled)
206
{
207
int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
208
if (lines_skippable > 0)
209
{
210
int lines_skipped = 0;
211
while (line < text_end && lines_skipped < lines_skippable)
212
{
213
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
214
if (!line_end)
215
line_end = text_end;
216
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
217
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
218
line = line_end + 1;
219
lines_skipped++;
220
}
221
pos.y += lines_skipped * line_height;
222
}
223
}
224
225
// Lines to render
226
if (line < text_end)
227
{
228
ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
229
while (line < text_end)
230
{
231
if (IsClippedEx(line_rect, 0))
232
break;
233
234
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
235
if (!line_end)
236
line_end = text_end;
237
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
238
RenderText(pos, line, line_end, false);
239
line = line_end + 1;
240
line_rect.Min.y += line_height;
241
line_rect.Max.y += line_height;
242
pos.y += line_height;
243
}
244
245
// Count remaining lines
246
int lines_skipped = 0;
247
while (line < text_end)
248
{
249
const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line);
250
if (!line_end)
251
line_end = text_end;
252
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
253
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
254
line = line_end + 1;
255
lines_skipped++;
256
}
257
pos.y += lines_skipped * line_height;
258
}
259
text_size.y = (pos - text_pos).y;
260
261
ImRect bb(text_pos, text_pos + text_size);
262
ItemSize(text_size, 0.0f);
263
ItemAdd(bb, 0);
264
}
265
}
266
267
void ImGui::TextUnformatted(const char* text, const char* text_end)
268
{
269
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
270
}
271
272
void ImGui::Text(const char* fmt, ...)
273
{
274
va_list args;
275
va_start(args, fmt);
276
TextV(fmt, args);
277
va_end(args);
278
}
279
280
void ImGui::TextV(const char* fmt, va_list args)
281
{
282
ImGuiWindow* window = GetCurrentWindow();
283
if (window->SkipItems)
284
return;
285
286
const char* text, *text_end;
287
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
288
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
289
}
290
291
void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
292
{
293
va_list args;
294
va_start(args, fmt);
295
TextColoredV(col, fmt, args);
296
va_end(args);
297
}
298
299
void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
300
{
301
PushStyleColor(ImGuiCol_Text, col);
302
TextV(fmt, args);
303
PopStyleColor();
304
}
305
306
void ImGui::TextDisabled(const char* fmt, ...)
307
{
308
va_list args;
309
va_start(args, fmt);
310
TextDisabledV(fmt, args);
311
va_end(args);
312
}
313
314
void ImGui::TextDisabledV(const char* fmt, va_list args)
315
{
316
ImGuiContext& g = *GImGui;
317
PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
318
TextV(fmt, args);
319
PopStyleColor();
320
}
321
322
void ImGui::TextWrapped(const char* fmt, ...)
323
{
324
va_list args;
325
va_start(args, fmt);
326
TextWrappedV(fmt, args);
327
va_end(args);
328
}
329
330
void ImGui::TextWrappedV(const char* fmt, va_list args)
331
{
332
ImGuiContext& g = *GImGui;
333
const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
334
if (need_backup)
335
PushTextWrapPos(0.0f);
336
TextV(fmt, args);
337
if (need_backup)
338
PopTextWrapPos();
339
}
340
341
void ImGui::TextAligned(float align_x, float size_x, const char* fmt, ...)
342
{
343
va_list args;
344
va_start(args, fmt);
345
TextAlignedV(align_x, size_x, fmt, args);
346
va_end(args);
347
}
348
349
// align_x: 0.0f = left, 0.5f = center, 1.0f = right.
350
// size_x : 0.0f = shortcut for GetContentRegionAvail().x
351
// FIXME-WIP: Works but API is likely to be reworked. This is designed for 1 item on the line. (#7024)
352
void ImGui::TextAlignedV(float align_x, float size_x, const char* fmt, va_list args)
353
{
354
ImGuiWindow* window = GetCurrentWindow();
355
if (window->SkipItems)
356
return;
357
358
const char* text, *text_end;
359
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
360
const ImVec2 text_size = CalcTextSize(text, text_end);
361
size_x = CalcItemSize(ImVec2(size_x, 0.0f), 0.0f, text_size.y).x;
362
363
ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
364
ImVec2 pos_max(pos.x + size_x, window->ClipRect.Max.y);
365
ImVec2 size(ImMin(size_x, text_size.x), text_size.y);
366
window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, pos.x + text_size.x);
367
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, pos.x + text_size.x);
368
if (align_x > 0.0f && text_size.x < size_x)
369
pos.x += ImTrunc((size_x - text_size.x) * align_x);
370
RenderTextEllipsis(window->DrawList, pos, pos_max, pos_max.x, text, text_end, &text_size);
371
372
const ImVec2 backup_max_pos = window->DC.CursorMaxPos;
373
ItemSize(size);
374
ItemAdd(ImRect(pos, pos + size), 0);
375
window->DC.CursorMaxPos.x = backup_max_pos.x; // Cancel out extending content size because right-aligned text would otherwise mess it up.
376
377
if (size_x < text_size.x && IsItemHovered(ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_ForTooltip))
378
SetTooltip("%.*s", (int)(text_end - text), text);
379
}
380
381
void ImGui::LabelText(const char* label, const char* fmt, ...)
382
{
383
va_list args;
384
va_start(args, fmt);
385
LabelTextV(label, fmt, args);
386
va_end(args);
387
}
388
389
// Add a label+text combo aligned to other label+value widgets
390
void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
391
{
392
ImGuiWindow* window = GetCurrentWindow();
393
if (window->SkipItems)
394
return;
395
396
ImGuiContext& g = *GImGui;
397
const ImGuiStyle& style = g.Style;
398
const float w = CalcItemWidth();
399
400
const char* value_text_begin, *value_text_end;
401
ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
402
const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
403
const ImVec2 label_size = CalcTextSize(label, NULL, true);
404
405
const ImVec2 pos = window->DC.CursorPos;
406
const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
407
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));
408
ItemSize(total_bb, style.FramePadding.y);
409
if (!ItemAdd(total_bb, 0))
410
return;
411
412
// Render
413
RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
414
if (label_size.x > 0.0f)
415
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
416
}
417
418
void ImGui::BulletText(const char* fmt, ...)
419
{
420
va_list args;
421
va_start(args, fmt);
422
BulletTextV(fmt, args);
423
va_end(args);
424
}
425
426
// Text with a little bullet aligned to the typical tree node.
427
void ImGui::BulletTextV(const char* fmt, va_list args)
428
{
429
ImGuiWindow* window = GetCurrentWindow();
430
if (window->SkipItems)
431
return;
432
433
ImGuiContext& g = *GImGui;
434
const ImGuiStyle& style = g.Style;
435
436
const char* text_begin, *text_end;
437
ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
438
const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
439
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
440
ImVec2 pos = window->DC.CursorPos;
441
pos.y += window->DC.CurrLineTextBaseOffset;
442
ItemSize(total_size, 0.0f);
443
const ImRect bb(pos, pos + total_size);
444
if (!ItemAdd(bb, 0))
445
return;
446
447
// Render
448
ImU32 text_col = GetColorU32(ImGuiCol_Text);
449
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
450
RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
451
}
452
453
//-------------------------------------------------------------------------
454
// [SECTION] Widgets: Main
455
//-------------------------------------------------------------------------
456
// - ButtonBehavior() [Internal]
457
// - Button()
458
// - SmallButton()
459
// - InvisibleButton()
460
// - ArrowButton()
461
// - CloseButton() [Internal]
462
// - CollapseButton() [Internal]
463
// - GetWindowScrollbarID() [Internal]
464
// - GetWindowScrollbarRect() [Internal]
465
// - Scrollbar() [Internal]
466
// - ScrollbarEx() [Internal]
467
// - Image()
468
// - ImageButton()
469
// - Checkbox()
470
// - CheckboxFlagsT() [Internal]
471
// - CheckboxFlags()
472
// - RadioButton()
473
// - ProgressBar()
474
// - Bullet()
475
// - Hyperlink()
476
//-------------------------------------------------------------------------
477
478
// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
479
// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
480
// this code is a little complex.
481
// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
482
// See the series of events below and the corresponding state reported by dear imgui:
483
//------------------------------------------------------------------------------------------------------------------------------------------------
484
// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
485
// Frame N+0 (mouse is outside bb) - - - - - -
486
// Frame N+1 (mouse moves inside bb) - true - - - -
487
// Frame N+2 (mouse button is down) - true true true - true
488
// Frame N+3 (mouse button is down) - true true - - -
489
// Frame N+4 (mouse moves outside bb) - - true - - -
490
// Frame N+5 (mouse moves inside bb) - true true - - -
491
// Frame N+6 (mouse button is released) true true - - true -
492
// Frame N+7 (mouse button is released) - true - - - -
493
// Frame N+8 (mouse moves outside bb) - - - - - -
494
//------------------------------------------------------------------------------------------------------------------------------------------------
495
// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
496
// Frame N+2 (mouse button is down) true true true true - true
497
// Frame N+3 (mouse button is down) - true true - - -
498
// Frame N+6 (mouse button is released) - true - - true -
499
// Frame N+7 (mouse button is released) - true - - - -
500
//------------------------------------------------------------------------------------------------------------------------------------------------
501
// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
502
// Frame N+2 (mouse button is down) - true - - - true
503
// Frame N+3 (mouse button is down) - true - - - -
504
// Frame N+6 (mouse button is released) true true - - - -
505
// Frame N+7 (mouse button is released) - true - - - -
506
//------------------------------------------------------------------------------------------------------------------------------------------------
507
// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
508
// Frame N+0 (mouse button is down) - true - - - true
509
// Frame N+1 (mouse button is down) - true - - - -
510
// Frame N+2 (mouse button is released) - true - - - -
511
// Frame N+3 (mouse button is released) - true - - - -
512
// Frame N+4 (mouse button is down) true true true true - true
513
// Frame N+5 (mouse button is down) - true true - - -
514
// Frame N+6 (mouse button is released) - true - - true -
515
// Frame N+7 (mouse button is released) - true - - - -
516
//------------------------------------------------------------------------------------------------------------------------------------------------
517
// Note that some combinations are supported,
518
// - PressedOnDragDropHold can generally be associated with any flag.
519
// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
520
//------------------------------------------------------------------------------------------------------------------------------------------------
521
// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set:
522
// Repeat+ Repeat+ Repeat+ Repeat+
523
// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
524
//-------------------------------------------------------------------------------------------------------------------------------------------------
525
// Frame N+0 (mouse button is down) - true - true
526
// ... - - - -
527
// Frame N + RepeatDelay true true - true
528
// ... - - - -
529
// Frame N + RepeatDelay + RepeatRate*N true true - true
530
//-------------------------------------------------------------------------------------------------------------------------------------------------
531
532
// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
533
// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
534
// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
535
// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
536
// One idiom which was previously valid which will now emit a warning is when using multiple overlaid ButtonBehavior()
537
// with same ID and different MouseButton (see #8030). You can fix it by:
538
// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
539
// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
540
bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
541
{
542
ImGuiContext& g = *GImGui;
543
ImGuiWindow* window = GetCurrentWindow();
544
545
// Default behavior inherited from item flags
546
// Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
547
ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
548
if (flags & ImGuiButtonFlags_AllowOverlap)
549
item_flags |= ImGuiItemFlags_AllowOverlap;
550
if (item_flags & ImGuiItemFlags_NoFocus)
551
flags |= ImGuiButtonFlags_NoFocus | ImGuiButtonFlags_NoNavFocus;
552
553
// Default only reacts to left mouse button
554
if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
555
flags |= ImGuiButtonFlags_MouseButtonLeft;
556
557
// Default behavior requires click + release inside bounding box
558
if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
559
flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
560
561
ImGuiWindow* backup_hovered_window = g.HoveredWindow;
562
const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window->RootWindow;
563
if (flatten_hovered_children)
564
g.HoveredWindow = window;
565
566
#ifdef IMGUI_ENABLE_TEST_ENGINE
567
// Alternate registration spot, for when caller didn't use ItemAdd()
568
if (g.LastItemData.ID != id)
569
IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
570
#endif
571
572
bool pressed = false;
573
bool hovered = ItemHoverable(bb, id, item_flags);
574
575
// Special mode for Drag and Drop used by openables (tree nodes, tabs etc.)
576
// where holding the button pressed for a long time while drag a payload item triggers the button.
577
if (g.DragDropActive)
578
{
579
if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
580
{
581
hovered = true;
582
SetHoveredID(id);
583
if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
584
{
585
pressed = true;
586
g.DragDropHoldJustPressedId = id;
587
FocusWindow(window);
588
}
589
}
590
if (g.DragDropAcceptIdPrev == id && (g.DragDropAcceptFlagsPrev & ImGuiDragDropFlags_AcceptDrawAsHovered))
591
hovered = true;
592
}
593
594
if (flatten_hovered_children)
595
g.HoveredWindow = backup_hovered_window;
596
597
// Mouse handling
598
const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
599
if (hovered)
600
{
601
IM_ASSERT(id != 0); // Lazily check inside rare path.
602
603
// Poll mouse buttons
604
// - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
605
// - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
606
int mouse_button_clicked = -1;
607
int mouse_button_released = -1;
608
for (int button = 0; button < 3; button++)
609
if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
610
{
611
if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
612
if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
613
}
614
615
// Process initial action
616
const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
617
if (mods_ok)
618
{
619
if (mouse_button_clicked != -1 && g.ActiveId != id)
620
{
621
if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
622
SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
623
if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
624
{
625
SetActiveID(id, window);
626
g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
627
if (!(flags & ImGuiButtonFlags_NoNavFocus))
628
{
629
SetFocusID(id, window);
630
FocusWindow(window);
631
}
632
else if (!(flags & ImGuiButtonFlags_NoFocus))
633
{
634
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
635
}
636
}
637
if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
638
{
639
pressed = true;
640
if (flags & ImGuiButtonFlags_NoHoldingActiveId)
641
ClearActiveID();
642
else
643
SetActiveID(id, window); // Hold on ID
644
g.ActiveIdMouseButton = (ImS8)mouse_button_clicked;
645
if (!(flags & ImGuiButtonFlags_NoNavFocus))
646
{
647
SetFocusID(id, window);
648
FocusWindow(window);
649
}
650
else if (!(flags & ImGuiButtonFlags_NoFocus))
651
{
652
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
653
}
654
}
655
}
656
if (flags & ImGuiButtonFlags_PressedOnRelease)
657
{
658
if (mouse_button_released != -1)
659
{
660
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
661
if (!has_repeated_at_least_once)
662
pressed = true;
663
if (!(flags & ImGuiButtonFlags_NoNavFocus))
664
SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
665
ClearActiveID();
666
}
667
}
668
669
// 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
670
// Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
671
if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
672
if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
673
pressed = true;
674
}
675
676
if (pressed && g.IO.ConfigNavCursorVisibleAuto)
677
g.NavCursorVisible = false;
678
}
679
680
// Keyboard/Gamepad navigation handling
681
// We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
682
if ((item_flags & ImGuiItemFlags_Disabled) == 0)
683
{
684
if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
685
if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
686
hovered = true;
687
if (g.NavActivateDownId == id)
688
{
689
bool nav_activated_by_code = (g.NavActivateId == id);
690
bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
691
if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
692
{
693
// Avoid pressing multiple keys from triggering excessive amount of repeat events
694
const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
695
const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
696
const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
697
const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
698
nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
699
}
700
if (nav_activated_by_code || nav_activated_by_inputs)
701
{
702
// Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
703
pressed = true;
704
SetActiveID(id, window);
705
g.ActiveIdSource = g.NavInputSource;
706
if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
707
SetFocusID(id, window);
708
if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
709
g.ActiveIdFromShortcut = true;
710
}
711
}
712
}
713
714
// Process while held
715
bool held = false;
716
if (g.ActiveId == id)
717
{
718
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
719
{
720
if (g.ActiveIdIsJustActivated)
721
g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
722
723
const int mouse_button = g.ActiveIdMouseButton;
724
if (mouse_button == -1)
725
{
726
// Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
727
ClearActiveID();
728
}
729
else if (IsMouseDown(mouse_button, test_owner_id))
730
{
731
held = true;
732
}
733
else
734
{
735
bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
736
bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
737
if ((release_in || release_anywhere) && !g.DragDropActive)
738
{
739
// Report as pressed when releasing the mouse (this is the most common path)
740
bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
741
bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
742
bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
743
if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
744
pressed = true;
745
}
746
ClearActiveID();
747
}
748
if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
749
g.NavCursorVisible = false;
750
}
751
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
752
{
753
// When activated using Nav, we hold on the ActiveID until activation button is released
754
if (g.NavActivateDownId == id)
755
held = true; // hovered == true not true as we are already likely hovered on direct activation.
756
else
757
ClearActiveID();
758
}
759
if (pressed)
760
g.ActiveIdHasBeenPressedBefore = true;
761
}
762
763
// Activation highlight (this may be a remote activation)
764
if (g.NavHighlightActivatedId == id && (item_flags & ImGuiItemFlags_Disabled) == 0)
765
hovered = true;
766
767
if (out_hovered) *out_hovered = hovered;
768
if (out_held) *out_held = held;
769
770
return pressed;
771
}
772
773
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
774
{
775
ImGuiWindow* window = GetCurrentWindow();
776
if (window->SkipItems)
777
return false;
778
779
ImGuiContext& g = *GImGui;
780
const ImGuiStyle& style = g.Style;
781
const ImGuiID id = window->GetID(label);
782
const ImVec2 label_size = CalcTextSize(label, NULL, true);
783
784
ImVec2 pos = window->DC.CursorPos;
785
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)
786
pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
787
ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
788
789
const ImRect bb(pos, pos + size);
790
ItemSize(size, style.FramePadding.y);
791
if (!ItemAdd(bb, id))
792
return false;
793
794
bool hovered, held;
795
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
796
797
// Render
798
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
799
RenderNavCursor(bb, id);
800
RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
801
802
if (g.LogEnabled)
803
LogSetNextTextDecoration("[", "]");
804
RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb);
805
806
// Automatically close popups
807
//if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
808
// CloseCurrentPopup();
809
810
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
811
return pressed;
812
}
813
814
bool ImGui::Button(const char* label, const ImVec2& size_arg)
815
{
816
return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
817
}
818
819
// Small buttons fits within text without additional vertical spacing.
820
bool ImGui::SmallButton(const char* label)
821
{
822
ImGuiContext& g = *GImGui;
823
float backup_padding_y = g.Style.FramePadding.y;
824
g.Style.FramePadding.y = 0.0f;
825
bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
826
g.Style.FramePadding.y = backup_padding_y;
827
return pressed;
828
}
829
830
// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
831
// 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)
832
bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
833
{
834
ImGuiContext& g = *GImGui;
835
ImGuiWindow* window = GetCurrentWindow();
836
if (window->SkipItems)
837
return false;
838
839
// Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
840
IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
841
842
const ImGuiID id = window->GetID(str_id);
843
ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f);
844
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
845
ItemSize(size);
846
if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
847
return false;
848
849
bool hovered, held;
850
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
851
RenderNavCursor(bb, id);
852
853
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
854
return pressed;
855
}
856
857
bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
858
{
859
ImGuiContext& g = *GImGui;
860
ImGuiWindow* window = GetCurrentWindow();
861
if (window->SkipItems)
862
return false;
863
864
const ImGuiID id = window->GetID(str_id);
865
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
866
const float default_size = GetFrameHeight();
867
ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
868
if (!ItemAdd(bb, id))
869
return false;
870
871
bool hovered, held;
872
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
873
874
// Render
875
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
876
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
877
RenderNavCursor(bb, id);
878
RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
879
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);
880
881
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
882
return pressed;
883
}
884
885
bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
886
{
887
float sz = GetFrameHeight();
888
return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
889
}
890
891
// Button to close a window
892
bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
893
{
894
ImGuiContext& g = *GImGui;
895
ImGuiWindow* window = g.CurrentWindow;
896
897
// 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)
898
// 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?
899
const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
900
ImRect bb_interact = bb;
901
const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
902
if (area_to_visible_ratio < 1.5f)
903
bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
904
905
// Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
906
// (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).
907
bool is_clipped = !ItemAdd(bb_interact, id);
908
909
bool hovered, held;
910
bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
911
if (is_clipped)
912
return pressed;
913
914
// Render
915
ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
916
if (hovered)
917
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
918
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
919
const ImU32 cross_col = GetColorU32(ImGuiCol_Text);
920
const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
921
const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
922
const float cross_thickness = 1.0f; // FIXME-DPI
923
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness);
924
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness);
925
926
return pressed;
927
}
928
929
bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos)
930
{
931
ImGuiContext& g = *GImGui;
932
ImGuiWindow* window = g.CurrentWindow;
933
934
ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
935
bool is_clipped = !ItemAdd(bb, id);
936
bool hovered, held;
937
bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
938
if (is_clipped)
939
return pressed;
940
941
// Render
942
ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
943
ImU32 text_col = GetColorU32(ImGuiCol_Text);
944
if (hovered || held)
945
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
946
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
947
RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
948
949
// Switch to moving the window after mouse is moved beyond the initial drag threshold
950
if (IsItemActive() && IsMouseDragging(0))
951
StartMouseMovingWindow(window);
952
953
return pressed;
954
}
955
956
ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
957
{
958
return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
959
}
960
961
// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
962
ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
963
{
964
ImGuiContext& g = *GImGui;
965
const ImRect outer_rect = window->Rect();
966
const ImRect inner_rect = window->InnerRect;
967
const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
968
IM_ASSERT(scrollbar_size >= 0.0f);
969
const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f);
970
const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f;
971
if (axis == ImGuiAxis_X)
972
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);
973
else
974
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);
975
}
976
977
void ImGui::Scrollbar(ImGuiAxis axis)
978
{
979
ImGuiContext& g = *GImGui;
980
ImGuiWindow* window = g.CurrentWindow;
981
const ImGuiID id = GetWindowScrollbarID(window, axis);
982
983
// Calculate scrollbar bounding box
984
ImRect bb = GetWindowScrollbarRect(window, axis);
985
ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
986
if (axis == ImGuiAxis_X)
987
{
988
rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
989
if (!window->ScrollbarY)
990
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
991
}
992
else
993
{
994
if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
995
rounding_corners |= ImDrawFlags_RoundCornersTopRight;
996
if (!window->ScrollbarX)
997
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
998
}
999
float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
1000
float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
1001
ImS64 scroll = (ImS64)window->ScrollExpected[axis];
1002
ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
1003
window->ScrollExpected[axis] = (float)scroll;
1004
}
1005
1006
// Vertical/Horizontal scrollbar
1007
// The entire piece of code below is rather confusing because:
1008
// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
1009
// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
1010
// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
1011
// Still, the code should probably be made simpler..
1012
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)
1013
{
1014
ImGuiContext& g = *GImGui;
1015
ImGuiWindow* window = g.CurrentWindow;
1016
if (window->SkipItems)
1017
return false;
1018
1019
const float bb_frame_width = bb_frame.GetWidth();
1020
const float bb_frame_height = bb_frame.GetHeight();
1021
if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
1022
return false;
1023
1024
// 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)
1025
float alpha = 1.0f;
1026
if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width)
1027
alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f));
1028
if (alpha <= 0.0f)
1029
return false;
1030
1031
const ImGuiStyle& style = g.Style;
1032
const bool allow_interaction = (alpha >= 1.0f);
1033
1034
ImRect bb = bb_frame;
1035
float padding = IM_TRUNC(ImMin(style.ScrollbarPadding, ImMin(bb_frame_width, bb_frame_height) * 0.5f));
1036
bb.Expand(-padding);
1037
1038
// V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
1039
const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
1040
if (scrollbar_size_v < 1.0f)
1041
return false;
1042
1043
// Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
1044
// But we maintain a minimum size in pixel to allow for the user to still aim inside.
1045
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.
1046
const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
1047
const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize);
1048
const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v);
1049
const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
1050
1051
// Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
1052
bool held = false;
1053
bool hovered = false;
1054
ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
1055
ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
1056
1057
const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1058
float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1059
float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1060
if (held && allow_interaction && grab_h_norm < 1.0f)
1061
{
1062
const float scrollbar_pos_v = bb.Min[axis];
1063
const float mouse_pos_v = g.IO.MousePos[axis];
1064
1065
// Click position in scrollbar normalized space (0.0f->1.0f)
1066
const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1067
1068
const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1069
if (g.ActiveIdIsJustActivated)
1070
{
1071
// On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1072
const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1073
g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1074
g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1075
}
1076
1077
// Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1078
// 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
1079
if (g.ScrollbarSeekMode == 0)
1080
{
1081
// Absolute seeking
1082
const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1083
*p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1084
}
1085
else
1086
{
1087
// Page by page
1088
if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1089
{
1090
float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1091
*p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1092
}
1093
}
1094
1095
// Update values for rendering
1096
scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1097
grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1098
g.ScrollbarHeld |= 2;
1099
1100
// Update distance to grab now that we have seek'ed and saturated
1101
//if (seek_absolute)
1102
// g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1103
}
1104
1105
// Render
1106
const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1107
const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1108
window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1109
ImRect grab_rect;
1110
if (axis == ImGuiAxis_X)
1111
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);
1112
else
1113
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);
1114
window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1115
1116
return held;
1117
}
1118
1119
// - Read about ImTextureID/ImTextureRef here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1120
// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1121
void ImGui::ImageWithBg(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1122
{
1123
ImGuiContext& g = *GImGui;
1124
ImGuiWindow* window = GetCurrentWindow();
1125
if (window->SkipItems)
1126
return;
1127
1128
const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize);
1129
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1130
ItemSize(bb);
1131
if (!ItemAdd(bb, 0))
1132
return;
1133
1134
// Render
1135
if (g.Style.ImageBorderSize > 0.0f)
1136
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, ImDrawFlags_None, g.Style.ImageBorderSize);
1137
if (bg_col.w > 0.0f)
1138
window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1139
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1140
}
1141
1142
void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1)
1143
{
1144
ImageWithBg(tex_ref, image_size, uv0, uv1);
1145
}
1146
1147
// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238)
1148
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1149
void ImGui::Image(ImTextureRef tex_ref, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1150
{
1151
ImGuiContext& g = *GImGui;
1152
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
1153
PushStyleColor(ImGuiCol_Border, border_col);
1154
ImageWithBg(tex_ref, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col);
1155
PopStyleColor();
1156
PopStyleVar();
1157
}
1158
#endif
1159
1160
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)
1161
{
1162
ImGuiContext& g = *GImGui;
1163
ImGuiWindow* window = GetCurrentWindow();
1164
if (window->SkipItems)
1165
return false;
1166
1167
const ImVec2 padding = g.Style.FramePadding;
1168
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1169
ItemSize(bb);
1170
if (!ItemAdd(bb, id))
1171
return false;
1172
1173
bool hovered, held;
1174
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1175
1176
// Render
1177
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1178
RenderNavCursor(bb, id);
1179
RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding));
1180
if (bg_col.w > 0.0f)
1181
window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1182
window->DrawList->AddImage(tex_ref, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1183
1184
return pressed;
1185
}
1186
1187
// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1188
// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1189
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)
1190
{
1191
ImGuiContext& g = *GImGui;
1192
ImGuiWindow* window = g.CurrentWindow;
1193
if (window->SkipItems)
1194
return false;
1195
1196
return ImageButtonEx(window->GetID(str_id), tex_ref, image_size, uv0, uv1, bg_col, tint_col);
1197
}
1198
1199
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1200
// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1201
// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1202
// - new ImageButton() requires an explicit 'const char* str_id'
1203
// - old ImageButton() had frame_padding' override argument.
1204
// - new ImageButton() always use style.FramePadding.
1205
/*
1206
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)
1207
{
1208
// Default to using texture ID as ID. User can still push string/integer prefixes.
1209
PushID((ImTextureID)(intptr_t)user_texture_id);
1210
if (frame_padding >= 0)
1211
PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1212
bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1213
if (frame_padding >= 0)
1214
PopStyleVar();
1215
PopID();
1216
return ret;
1217
}
1218
*/
1219
#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1220
1221
bool ImGui::Checkbox(const char* label, bool* v)
1222
{
1223
ImGuiWindow* window = GetCurrentWindow();
1224
if (window->SkipItems)
1225
return false;
1226
1227
ImGuiContext& g = *GImGui;
1228
const ImGuiStyle& style = g.Style;
1229
const ImGuiID id = window->GetID(label);
1230
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1231
1232
const float square_sz = GetFrameHeight();
1233
const ImVec2 pos = window->DC.CursorPos;
1234
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));
1235
ItemSize(total_bb, style.FramePadding.y);
1236
const bool is_visible = ItemAdd(total_bb, id);
1237
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1238
if (!is_visible)
1239
if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1240
{
1241
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1242
return false;
1243
}
1244
1245
// Range-Selection/Multi-selection support (header)
1246
bool checked = *v;
1247
if (is_multi_select)
1248
MultiSelectItemHeader(id, &checked, NULL);
1249
1250
bool hovered, held;
1251
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1252
1253
// Range-Selection/Multi-selection support (footer)
1254
if (is_multi_select)
1255
MultiSelectItemFooter(id, &checked, &pressed);
1256
else if (pressed)
1257
checked = !checked;
1258
1259
if (*v != checked)
1260
{
1261
*v = checked;
1262
pressed = true; // return value
1263
MarkItemEdited(id);
1264
}
1265
1266
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1267
const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1268
if (is_visible)
1269
{
1270
RenderNavCursor(total_bb, id);
1271
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
1272
ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1273
if (mixed_value)
1274
{
1275
// Undocumented tristate/mixed/indeterminate checkbox (#2644)
1276
// This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1277
ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1278
window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1279
}
1280
else if (*v)
1281
{
1282
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1283
RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1284
}
1285
}
1286
const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1287
if (g.LogEnabled)
1288
LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1289
if (is_visible && label_size.x > 0.0f)
1290
RenderText(label_pos, label);
1291
1292
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1293
return pressed;
1294
}
1295
1296
template<typename T>
1297
bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1298
{
1299
bool all_on = (*flags & flags_value) == flags_value;
1300
bool any_on = (*flags & flags_value) != 0;
1301
bool pressed;
1302
if (!all_on && any_on)
1303
{
1304
ImGuiContext& g = *GImGui;
1305
g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1306
pressed = Checkbox(label, &all_on);
1307
}
1308
else
1309
{
1310
pressed = Checkbox(label, &all_on);
1311
1312
}
1313
if (pressed)
1314
{
1315
if (all_on)
1316
*flags |= flags_value;
1317
else
1318
*flags &= ~flags_value;
1319
}
1320
return pressed;
1321
}
1322
1323
bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1324
{
1325
return CheckboxFlagsT(label, flags, flags_value);
1326
}
1327
1328
bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1329
{
1330
return CheckboxFlagsT(label, flags, flags_value);
1331
}
1332
1333
bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1334
{
1335
return CheckboxFlagsT(label, flags, flags_value);
1336
}
1337
1338
bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1339
{
1340
return CheckboxFlagsT(label, flags, flags_value);
1341
}
1342
1343
bool ImGui::RadioButton(const char* label, bool active)
1344
{
1345
ImGuiWindow* window = GetCurrentWindow();
1346
if (window->SkipItems)
1347
return false;
1348
1349
ImGuiContext& g = *GImGui;
1350
const ImGuiStyle& style = g.Style;
1351
const ImGuiID id = window->GetID(label);
1352
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1353
1354
const float square_sz = GetFrameHeight();
1355
const ImVec2 pos = window->DC.CursorPos;
1356
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1357
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));
1358
ItemSize(total_bb, style.FramePadding.y);
1359
if (!ItemAdd(total_bb, id))
1360
return false;
1361
1362
ImVec2 center = check_bb.GetCenter();
1363
center.x = IM_ROUND(center.x);
1364
center.y = IM_ROUND(center.y);
1365
const float radius = (square_sz - 1.0f) * 0.5f;
1366
1367
bool hovered, held;
1368
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1369
if (pressed)
1370
MarkItemEdited(id);
1371
1372
RenderNavCursor(total_bb, id);
1373
const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1374
window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1375
if (active)
1376
{
1377
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1378
window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1379
}
1380
1381
if (style.FrameBorderSize > 0.0f)
1382
{
1383
window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1384
window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1385
}
1386
1387
ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1388
if (g.LogEnabled)
1389
LogRenderedText(&label_pos, active ? "(x)" : "( )");
1390
if (label_size.x > 0.0f)
1391
RenderText(label_pos, label);
1392
1393
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1394
return pressed;
1395
}
1396
1397
// 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..
1398
bool ImGui::RadioButton(const char* label, int* v, int v_button)
1399
{
1400
const bool pressed = RadioButton(label, *v == v_button);
1401
if (pressed)
1402
*v = v_button;
1403
return pressed;
1404
}
1405
1406
// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1407
void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1408
{
1409
ImGuiWindow* window = GetCurrentWindow();
1410
if (window->SkipItems)
1411
return;
1412
1413
ImGuiContext& g = *GImGui;
1414
const ImGuiStyle& style = g.Style;
1415
1416
ImVec2 pos = window->DC.CursorPos;
1417
ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1418
ImRect bb(pos, pos + size);
1419
ItemSize(size, style.FramePadding.y);
1420
if (!ItemAdd(bb, 0))
1421
return;
1422
1423
// Fraction < 0.0f will display an indeterminate progress bar animation
1424
// The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1425
const bool is_indeterminate = (fraction < 0.0f);
1426
if (!is_indeterminate)
1427
fraction = ImSaturate(fraction);
1428
1429
// Out of courtesy we accept a NaN fraction without crashing
1430
float fill_n0 = 0.0f;
1431
float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1432
1433
if (is_indeterminate)
1434
{
1435
const float fill_width_n = 0.2f;
1436
fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1437
fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1438
fill_n0 = ImSaturate(fill_n0);
1439
}
1440
1441
// Render
1442
RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1443
bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1444
float fill_x0 = ImLerp(bb.Min.x, bb.Max.x, fill_n0);
1445
float fill_x1 = ImLerp(bb.Min.x, bb.Max.x, fill_n1);
1446
if (fill_x0 < fill_x1)
1447
RenderRectFilledInRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_x0, fill_x1, style.FrameRounding);
1448
1449
// Default displaying the fraction as percentage string, but user can override it
1450
// Don't display text for indeterminate bars by default
1451
char overlay_buf[32];
1452
if (!is_indeterminate || overlay != NULL)
1453
{
1454
if (!overlay)
1455
{
1456
ImFormatString(overlay_buf, IM_COUNTOF(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1457
overlay = overlay_buf;
1458
}
1459
1460
ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1461
if (overlay_size.x > 0.0f)
1462
{
1463
float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : fill_x1 + style.ItemSpacing.x;
1464
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);
1465
}
1466
}
1467
}
1468
1469
void ImGui::Bullet()
1470
{
1471
ImGuiWindow* window = GetCurrentWindow();
1472
if (window->SkipItems)
1473
return;
1474
1475
ImGuiContext& g = *GImGui;
1476
const ImGuiStyle& style = g.Style;
1477
const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1478
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1479
ItemSize(bb);
1480
if (!ItemAdd(bb, 0))
1481
{
1482
SameLine(0, style.FramePadding.x * 2);
1483
return;
1484
}
1485
1486
// Render and stay on same line
1487
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1488
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1489
SameLine(0, style.FramePadding.x * 2.0f);
1490
}
1491
1492
// This is provided as a convenience for being an often requested feature.
1493
// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1494
// Because of this we currently don't provide many styling options for this widget
1495
// (e.g. hovered/active colors are automatically inferred from a single color).
1496
bool ImGui::TextLink(const char* label)
1497
{
1498
ImGuiWindow* window = GetCurrentWindow();
1499
if (window->SkipItems)
1500
return false;
1501
1502
ImGuiContext& g = *GImGui;
1503
const ImGuiID id = window->GetID(label);
1504
const char* label_end = FindRenderedTextEnd(label);
1505
1506
ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
1507
ImVec2 size = CalcTextSize(label, label_end, true);
1508
ImRect bb(pos, pos + size);
1509
ItemSize(size, 0.0f);
1510
if (!ItemAdd(bb, id))
1511
return false;
1512
1513
bool hovered, held;
1514
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1515
RenderNavCursor(bb, id);
1516
1517
if (hovered)
1518
SetMouseCursor(ImGuiMouseCursor_Hand);
1519
1520
ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1521
ImVec4 line_colf = text_colf;
1522
{
1523
// FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1524
// as we are currently experimenting/planning a different styling system.
1525
float h, s, v;
1526
ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1527
if (held || hovered)
1528
{
1529
v = ImSaturate(v + (held ? 0.4f : 0.3f));
1530
h = ImFmod(h + 0.02f, 1.0f);
1531
}
1532
ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1533
v = ImSaturate(v - 0.20f);
1534
ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1535
}
1536
1537
float line_y = bb.Max.y + ImFloor(g.FontBaked->Descent * g.FontBakedScale * 0.20f);
1538
window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode // FIXME-DPI
1539
1540
PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1541
RenderText(bb.Min, label, label_end);
1542
PopStyleColor();
1543
1544
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1545
return pressed;
1546
}
1547
1548
bool ImGui::TextLinkOpenURL(const char* label, const char* url)
1549
{
1550
ImGuiContext& g = *GImGui;
1551
if (url == NULL)
1552
url = label;
1553
bool pressed = TextLink(label);
1554
if (pressed && g.PlatformIO.Platform_OpenInShellFn != NULL)
1555
g.PlatformIO.Platform_OpenInShellFn(&g, url);
1556
SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1557
if (BeginPopupContextItem())
1558
{
1559
if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1560
SetClipboardText(url);
1561
EndPopup();
1562
}
1563
return pressed;
1564
}
1565
1566
//-------------------------------------------------------------------------
1567
// [SECTION] Widgets: Low-level Layout helpers
1568
//-------------------------------------------------------------------------
1569
// - Spacing()
1570
// - Dummy()
1571
// - NewLine()
1572
// - AlignTextToFramePadding()
1573
// - SeparatorEx() [Internal]
1574
// - Separator()
1575
// - SplitterBehavior() [Internal]
1576
// - ShrinkWidths() [Internal]
1577
//-------------------------------------------------------------------------
1578
1579
void ImGui::Spacing()
1580
{
1581
ImGuiWindow* window = GetCurrentWindow();
1582
if (window->SkipItems)
1583
return;
1584
ItemSize(ImVec2(0, 0));
1585
}
1586
1587
void ImGui::Dummy(const ImVec2& size)
1588
{
1589
ImGuiWindow* window = GetCurrentWindow();
1590
if (window->SkipItems)
1591
return;
1592
1593
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1594
ItemSize(size);
1595
ItemAdd(bb, 0);
1596
}
1597
1598
void ImGui::NewLine()
1599
{
1600
ImGuiWindow* window = GetCurrentWindow();
1601
if (window->SkipItems)
1602
return;
1603
1604
ImGuiContext& g = *GImGui;
1605
const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1606
window->DC.LayoutType = ImGuiLayoutType_Vertical;
1607
window->DC.IsSameLine = false;
1608
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.
1609
ItemSize(ImVec2(0, 0));
1610
else
1611
ItemSize(ImVec2(0.0f, g.FontSize));
1612
window->DC.LayoutType = backup_layout_type;
1613
}
1614
1615
void ImGui::AlignTextToFramePadding()
1616
{
1617
ImGuiWindow* window = GetCurrentWindow();
1618
if (window->SkipItems)
1619
return;
1620
1621
ImGuiContext& g = *GImGui;
1622
window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1623
window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1624
}
1625
1626
// Horizontal/vertical separating line
1627
// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1628
// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1629
void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1630
{
1631
ImGuiWindow* window = GetCurrentWindow();
1632
if (window->SkipItems)
1633
return;
1634
1635
ImGuiContext& g = *GImGui;
1636
IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1637
IM_ASSERT(thickness > 0.0f);
1638
1639
if (flags & ImGuiSeparatorFlags_Vertical)
1640
{
1641
// Vertical separator, for menu bars (use current line height).
1642
float y1 = window->DC.CursorPos.y;
1643
float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1644
const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1645
ItemSize(ImVec2(thickness, 0.0f));
1646
if (!ItemAdd(bb, 0))
1647
return;
1648
1649
// Draw
1650
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1651
if (g.LogEnabled)
1652
LogText(" |");
1653
}
1654
else if (flags & ImGuiSeparatorFlags_Horizontal)
1655
{
1656
// Horizontal Separator
1657
float x1 = window->DC.CursorPos.x;
1658
float x2 = window->WorkRect.Max.x;
1659
1660
// Preserve legacy behavior inside Columns()
1661
// Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1662
// We currently don't need to provide the same feature for tables because tables naturally have border features.
1663
ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1664
if (columns)
1665
{
1666
x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1667
x2 = window->Pos.x + window->Size.x;
1668
PushColumnsBackground();
1669
}
1670
1671
// We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1672
// FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1673
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.
1674
const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1675
ItemSize(ImVec2(0.0f, thickness_for_layout));
1676
1677
if (ItemAdd(bb, 0))
1678
{
1679
// Draw
1680
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1681
if (g.LogEnabled)
1682
LogRenderedText(&bb.Min, "--------------------------------\n");
1683
1684
}
1685
if (columns)
1686
{
1687
PopColumnsBackground();
1688
columns->LineMinY = window->DC.CursorPos.y;
1689
}
1690
}
1691
}
1692
1693
void ImGui::Separator()
1694
{
1695
ImGuiContext& g = *GImGui;
1696
ImGuiWindow* window = g.CurrentWindow;
1697
if (window->SkipItems)
1698
return;
1699
1700
// Those flags should eventually be configurable by the user
1701
// FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1702
ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1703
1704
// Only applies to legacy Columns() api as they relied on Separator() a lot.
1705
if (window->DC.CurrentColumns)
1706
flags |= ImGuiSeparatorFlags_SpanAllColumns;
1707
1708
SeparatorEx(flags, 1.0f);
1709
}
1710
1711
void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1712
{
1713
ImGuiContext& g = *GImGui;
1714
ImGuiWindow* window = g.CurrentWindow;
1715
ImGuiStyle& style = g.Style;
1716
1717
const ImVec2 label_size = CalcTextSize(label, label_end, false);
1718
const ImVec2 pos = window->DC.CursorPos;
1719
const ImVec2 padding = style.SeparatorTextPadding;
1720
1721
const float separator_thickness = style.SeparatorTextBorderSize;
1722
const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1723
const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1724
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));
1725
ItemSize(min_size, text_baseline_y);
1726
if (!ItemAdd(bb, id))
1727
return;
1728
1729
const float sep1_x1 = pos.x;
1730
const float sep2_x2 = bb.Max.x;
1731
const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1732
1733
const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1734
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
1735
1736
// This allows using SameLine() to position something in the 'extra_w'
1737
window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1738
1739
const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1740
if (label_size.x > 0.0f)
1741
{
1742
const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1743
const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1744
if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1745
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness);
1746
if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1747
window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1748
if (g.LogEnabled)
1749
LogSetNextTextDecoration("---", NULL);
1750
RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, label, label_end, &label_size);
1751
}
1752
else
1753
{
1754
if (g.LogEnabled)
1755
LogText("---");
1756
if (separator_thickness > 0.0f)
1757
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1758
}
1759
}
1760
1761
void ImGui::SeparatorText(const char* label)
1762
{
1763
ImGuiWindow* window = GetCurrentWindow();
1764
if (window->SkipItems)
1765
return;
1766
1767
// The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1768
// - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1769
// - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1770
// - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1771
// Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1772
// and then we can turn this into a format function.
1773
SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1774
}
1775
1776
// 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.
1777
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)
1778
{
1779
ImGuiContext& g = *GImGui;
1780
ImGuiWindow* window = g.CurrentWindow;
1781
1782
if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1783
return false;
1784
1785
// FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1786
// to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1787
// Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1788
ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1789
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1790
button_flags |= ImGuiButtonFlags_AllowOverlap;
1791
#endif
1792
1793
bool hovered, held;
1794
ImRect bb_interact = bb;
1795
bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1796
ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1797
if (hovered)
1798
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1799
1800
if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1801
SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1802
1803
ImRect bb_render = bb;
1804
if (held)
1805
{
1806
float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1807
1808
// Minimum pane size
1809
float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1810
float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1811
if (mouse_delta < -size_1_maximum_delta)
1812
mouse_delta = -size_1_maximum_delta;
1813
if (mouse_delta > size_2_maximum_delta)
1814
mouse_delta = size_2_maximum_delta;
1815
1816
// Apply resize
1817
if (mouse_delta != 0.0f)
1818
{
1819
*size1 = ImMax(*size1 + mouse_delta, min_size1);
1820
*size2 = ImMax(*size2 - mouse_delta, min_size2);
1821
bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1822
MarkItemEdited(id);
1823
}
1824
}
1825
1826
// Render at new position
1827
if (bg_col & IM_COL32_A_MASK)
1828
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1829
const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1830
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1831
1832
return held;
1833
}
1834
1835
static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1836
{
1837
const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1838
const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1839
if (int d = (int)(b->Width - a->Width))
1840
return d;
1841
return b->Index - a->Index;
1842
}
1843
1844
// Shrink excess width from a set of item, by removing width from the larger items first.
1845
// Set items Width to -1.0f to disable shrinking this item.
1846
void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess, float width_min)
1847
{
1848
if (count == 1)
1849
{
1850
if (items[0].Width >= 0.0f)
1851
items[0].Width = ImMax(items[0].Width - width_excess, width_min);
1852
return;
1853
}
1854
ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); // Sort largest first, smallest last.
1855
int count_same_width = 1;
1856
while (width_excess > 0.001f && count_same_width < count)
1857
{
1858
while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1859
count_same_width++;
1860
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);
1861
max_width_to_remove_per_item = ImMin(items[0].Width - width_min, max_width_to_remove_per_item);
1862
if (max_width_to_remove_per_item <= 0.0f)
1863
break;
1864
float base_width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1865
for (int item_n = 0; item_n < count_same_width; item_n++)
1866
{
1867
float width_to_remove_for_this_item = ImMin(base_width_to_remove_per_item, items[item_n].Width - width_min);
1868
items[item_n].Width -= width_to_remove_for_this_item;
1869
width_excess -= width_to_remove_for_this_item;
1870
}
1871
}
1872
1873
// Round width and redistribute remainder
1874
// 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.
1875
width_excess = 0.0f;
1876
for (int n = 0; n < count; n++)
1877
{
1878
float width_rounded = ImTrunc(items[n].Width);
1879
width_excess += items[n].Width - width_rounded;
1880
items[n].Width = width_rounded;
1881
}
1882
while (width_excess > 0.0f)
1883
for (int n = 0; n < count && width_excess > 0.0f; n++)
1884
{
1885
float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1886
items[n].Width += width_to_add;
1887
width_excess -= width_to_add;
1888
}
1889
}
1890
1891
//-------------------------------------------------------------------------
1892
// [SECTION] Widgets: ComboBox
1893
//-------------------------------------------------------------------------
1894
// - CalcMaxPopupHeightFromItemCount() [Internal]
1895
// - BeginCombo()
1896
// - BeginComboPopup() [Internal]
1897
// - EndCombo()
1898
// - BeginComboPreview() [Internal]
1899
// - EndComboPreview() [Internal]
1900
// - Combo()
1901
//-------------------------------------------------------------------------
1902
1903
static float CalcMaxPopupHeightFromItemCount(int items_count)
1904
{
1905
ImGuiContext& g = *GImGui;
1906
if (items_count <= 0)
1907
return FLT_MAX;
1908
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1909
}
1910
1911
bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1912
{
1913
ImGuiContext& g = *GImGui;
1914
ImGuiWindow* window = GetCurrentWindow();
1915
1916
ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
1917
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1918
if (window->SkipItems)
1919
return false;
1920
1921
const ImGuiStyle& style = g.Style;
1922
const ImGuiID id = window->GetID(label);
1923
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1924
if (flags & ImGuiComboFlags_WidthFitPreview)
1925
IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1926
1927
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1928
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1929
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f;
1930
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1931
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1932
const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1933
ItemSize(total_bb, style.FramePadding.y);
1934
if (!ItemAdd(total_bb, id, &bb))
1935
return false;
1936
1937
// Open on click
1938
bool hovered, held;
1939
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1940
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1941
bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1942
if (pressed && !popup_open)
1943
{
1944
OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1945
popup_open = true;
1946
}
1947
1948
// Render shape
1949
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1950
const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1951
RenderNavCursor(bb, id);
1952
if (!(flags & ImGuiComboFlags_NoPreview))
1953
window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1954
if (!(flags & ImGuiComboFlags_NoArrowButton))
1955
{
1956
ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1957
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1958
window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1959
if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1960
RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1961
}
1962
RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1963
1964
// Custom preview
1965
if (flags & ImGuiComboFlags_CustomPreview)
1966
{
1967
g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1968
IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1969
preview_value = NULL;
1970
}
1971
1972
// Render preview and label
1973
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1974
{
1975
if (g.LogEnabled)
1976
LogSetNextTextDecoration("{", "}");
1977
RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
1978
}
1979
if (label_size.x > 0)
1980
RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
1981
1982
if (!popup_open)
1983
return false;
1984
1985
g.NextWindowData.HasFlags = backup_next_window_data_flags;
1986
return BeginComboPopup(popup_id, bb, flags);
1987
}
1988
1989
bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1990
{
1991
ImGuiContext& g = *GImGui;
1992
if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
1993
{
1994
g.NextWindowData.ClearFlags();
1995
return false;
1996
}
1997
1998
// Set popup size
1999
float w = bb.GetWidth();
2000
if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
2001
{
2002
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
2003
}
2004
else
2005
{
2006
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
2007
flags |= ImGuiComboFlags_HeightRegular;
2008
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
2009
int popup_max_height_in_items = -1;
2010
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
2011
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
2012
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
2013
ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
2014
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
2015
constraint_min.x = w;
2016
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
2017
constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
2018
SetNextWindowSizeConstraints(constraint_min, constraint_max);
2019
}
2020
2021
// This is essentially a specialized version of BeginPopupEx()
2022
char name[16];
2023
ImFormatString(name, IM_COUNTOF(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
2024
2025
// Set position given a custom constraint (peak into expected window size so we can position it)
2026
// FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
2027
// FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
2028
if (ImGuiWindow* popup_window = FindWindowByName(name))
2029
if (popup_window->WasActive)
2030
{
2031
// Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
2032
ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
2033
popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
2034
ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
2035
ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
2036
SetNextWindowPos(pos);
2037
}
2038
2039
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
2040
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
2041
PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
2042
bool ret = Begin(name, NULL, window_flags);
2043
PopStyleVar();
2044
if (!ret)
2045
{
2046
EndPopup();
2047
if (!g.IO.ConfigDebugBeginReturnValueOnce && !g.IO.ConfigDebugBeginReturnValueLoop) // Begin may only return false with those debug tools activated.
2048
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
2049
return false;
2050
}
2051
g.BeginComboDepth++;
2052
return true;
2053
}
2054
2055
void ImGui::EndCombo()
2056
{
2057
ImGuiContext& g = *GImGui;
2058
EndPopup();
2059
g.BeginComboDepth--;
2060
}
2061
2062
// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
2063
// (Experimental, see GitHub issues: #1658, #4168)
2064
bool ImGui::BeginComboPreview()
2065
{
2066
ImGuiContext& g = *GImGui;
2067
ImGuiWindow* window = g.CurrentWindow;
2068
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2069
2070
if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
2071
return false;
2072
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?
2073
if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
2074
return false;
2075
2076
// FIXME: This could be contained in a PushWorkRect() api
2077
preview_data->BackupCursorPos = window->DC.CursorPos;
2078
preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
2079
preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
2080
preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
2081
preview_data->BackupLayout = window->DC.LayoutType;
2082
window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
2083
window->DC.CursorMaxPos = window->DC.CursorPos;
2084
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
2085
window->DC.IsSameLine = false;
2086
PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2087
2088
return true;
2089
}
2090
2091
void ImGui::EndComboPreview()
2092
{
2093
ImGuiContext& g = *GImGui;
2094
ImGuiWindow* window = g.CurrentWindow;
2095
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2096
2097
// FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2098
ImDrawList* draw_list = window->DrawList;
2099
if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2100
if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2101
{
2102
draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2103
draw_list->_TryMergeDrawCmds();
2104
}
2105
PopClipRect();
2106
window->DC.CursorPos = preview_data->BackupCursorPos;
2107
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2108
window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2109
window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2110
window->DC.LayoutType = preview_data->BackupLayout;
2111
window->DC.IsSameLine = false;
2112
preview_data->PreviewRect = ImRect();
2113
}
2114
2115
// Getter for the old Combo() API: const char*[]
2116
static const char* Items_ArrayGetter(void* data, int idx)
2117
{
2118
const char* const* items = (const char* const*)data;
2119
return items[idx];
2120
}
2121
2122
// Getter for the old Combo() API: "item1\0item2\0item3\0"
2123
static const char* Items_SingleStringGetter(void* data, int idx)
2124
{
2125
const char* items_separated_by_zeros = (const char*)data;
2126
int items_count = 0;
2127
const char* p = items_separated_by_zeros;
2128
while (*p)
2129
{
2130
if (idx == items_count)
2131
break;
2132
p += ImStrlen(p) + 1;
2133
items_count++;
2134
}
2135
return *p ? p : NULL;
2136
}
2137
2138
// Old API, prefer using BeginCombo() nowadays if you can.
2139
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)
2140
{
2141
ImGuiContext& g = *GImGui;
2142
2143
// Call the getter to obtain the preview string which is a parameter to BeginCombo()
2144
const char* preview_value = NULL;
2145
if (*current_item >= 0 && *current_item < items_count)
2146
preview_value = getter(user_data, *current_item);
2147
2148
// 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.
2149
if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2150
SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2151
2152
if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2153
return false;
2154
2155
// Display items
2156
bool value_changed = false;
2157
ImGuiListClipper clipper;
2158
clipper.Begin(items_count);
2159
clipper.IncludeItemByIndex(*current_item);
2160
while (clipper.Step())
2161
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2162
{
2163
const char* item_text = getter(user_data, i);
2164
if (item_text == NULL)
2165
item_text = "*Unknown item*";
2166
2167
PushID(i);
2168
const bool item_selected = (i == *current_item);
2169
if (Selectable(item_text, item_selected) && *current_item != i)
2170
{
2171
value_changed = true;
2172
*current_item = i;
2173
}
2174
if (item_selected)
2175
SetItemDefaultFocus();
2176
PopID();
2177
}
2178
2179
EndCombo();
2180
if (value_changed)
2181
MarkItemEdited(g.LastItemData.ID);
2182
2183
return value_changed;
2184
}
2185
2186
// Combo box helper allowing to pass an array of strings.
2187
bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2188
{
2189
const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2190
return value_changed;
2191
}
2192
2193
// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2194
bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2195
{
2196
int items_count = 0;
2197
const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2198
while (*p)
2199
{
2200
p += ImStrlen(p) + 1;
2201
items_count++;
2202
}
2203
bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2204
return value_changed;
2205
}
2206
2207
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2208
2209
struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2210
static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2211
{
2212
ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2213
const char* s = NULL;
2214
data->OldCallback(data->UserData, idx, &s);
2215
return s;
2216
}
2217
2218
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)
2219
{
2220
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2221
return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items);
2222
}
2223
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)
2224
{
2225
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2226
return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items);
2227
}
2228
2229
#endif
2230
2231
//-------------------------------------------------------------------------
2232
// [SECTION] Data Type and Data Formatting Helpers [Internal]
2233
//-------------------------------------------------------------------------
2234
// - DataTypeGetInfo()
2235
// - DataTypeFormatString()
2236
// - DataTypeApplyOp()
2237
// - DataTypeApplyFromText()
2238
// - DataTypeCompare()
2239
// - DataTypeClamp()
2240
// - GetMinimumStepAtDecimalPrecision
2241
// - RoundScalarWithFormat<>()
2242
//-------------------------------------------------------------------------
2243
2244
static const ImU32 GDefaultRgbaColorMarkers[4] =
2245
{
2246
IM_COL32(240,20,20,255), IM_COL32(20,240,20,255), IM_COL32(20,20,240,255), IM_COL32(140,140,140,255)
2247
};
2248
2249
static const ImGuiDataTypeInfo GDataTypeInfo[] =
2250
{
2251
{ sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2252
{ sizeof(unsigned char), "U8", "%u", "%u" },
2253
{ sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2254
{ sizeof(unsigned short), "U16", "%u", "%u" },
2255
{ sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2256
{ sizeof(unsigned int), "U32", "%u", "%u" },
2257
#ifdef _MSC_VER
2258
{ sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2259
{ sizeof(ImU64), "U64", "%I64u","%I64u" },
2260
#else
2261
{ sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2262
{ sizeof(ImU64), "U64", "%llu", "%llu" },
2263
#endif
2264
{ sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2265
{ sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2266
{ sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2267
{ 0, "char*","%s", "%s" }, // ImGuiDataType_String
2268
};
2269
IM_STATIC_ASSERT(IM_COUNTOF(GDataTypeInfo) == ImGuiDataType_COUNT);
2270
2271
const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2272
{
2273
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2274
return &GDataTypeInfo[data_type];
2275
}
2276
2277
int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2278
{
2279
// Signedness doesn't matter when pushing integer arguments
2280
if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2281
return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2282
if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2283
return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2284
if (data_type == ImGuiDataType_Float)
2285
return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2286
if (data_type == ImGuiDataType_Double)
2287
return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2288
if (data_type == ImGuiDataType_S8)
2289
return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2290
if (data_type == ImGuiDataType_U8)
2291
return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2292
if (data_type == ImGuiDataType_S16)
2293
return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2294
if (data_type == ImGuiDataType_U16)
2295
return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2296
IM_ASSERT(0);
2297
return 0;
2298
}
2299
2300
void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2301
{
2302
IM_ASSERT(op == '+' || op == '-');
2303
switch (data_type)
2304
{
2305
case ImGuiDataType_S8:
2306
if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2307
if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2308
return;
2309
case ImGuiDataType_U8:
2310
if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2311
if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2312
return;
2313
case ImGuiDataType_S16:
2314
if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2315
if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2316
return;
2317
case ImGuiDataType_U16:
2318
if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2319
if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2320
return;
2321
case ImGuiDataType_S32:
2322
if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2323
if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2324
return;
2325
case ImGuiDataType_U32:
2326
if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2327
if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2328
return;
2329
case ImGuiDataType_S64:
2330
if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2331
if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2332
return;
2333
case ImGuiDataType_U64:
2334
if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2335
if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2336
return;
2337
case ImGuiDataType_Float:
2338
if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2339
if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2340
return;
2341
case ImGuiDataType_Double:
2342
if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2343
if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2344
return;
2345
case ImGuiDataType_COUNT: break;
2346
}
2347
IM_ASSERT(0);
2348
}
2349
2350
// User can input math operators (e.g. +100) to edit a numerical values.
2351
// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2352
bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2353
{
2354
// Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2355
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2356
ImGuiDataTypeStorage data_backup;
2357
memcpy(&data_backup, p_data, type_info->Size);
2358
2359
while (ImCharIsBlankA(*buf))
2360
buf++;
2361
if (!buf[0])
2362
{
2363
if (p_data_when_empty != NULL)
2364
{
2365
memcpy(p_data, p_data_when_empty, type_info->Size);
2366
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2367
}
2368
return false;
2369
}
2370
2371
// Sanitize format
2372
// - 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
2373
// - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2374
char format_sanitized[32];
2375
if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2376
format = type_info->ScanFmt;
2377
else
2378
format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_COUNTOF(format_sanitized));
2379
2380
// Small types need a 32-bit buffer to receive the result from scanf()
2381
int v32 = 0;
2382
if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2383
return false;
2384
if (type_info->Size < 4)
2385
{
2386
if (data_type == ImGuiDataType_S8)
2387
*(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2388
else if (data_type == ImGuiDataType_U8)
2389
*(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2390
else if (data_type == ImGuiDataType_S16)
2391
*(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2392
else if (data_type == ImGuiDataType_U16)
2393
*(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2394
else
2395
IM_ASSERT(0);
2396
}
2397
2398
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2399
}
2400
2401
template<typename T>
2402
static int DataTypeCompareT(const T* lhs, const T* rhs)
2403
{
2404
if (*lhs < *rhs) return -1;
2405
if (*lhs > *rhs) return +1;
2406
return 0;
2407
}
2408
2409
int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2410
{
2411
switch (data_type)
2412
{
2413
case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2414
case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2415
case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2416
case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2417
case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2418
case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2419
case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2420
case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2421
case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2422
case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2423
case ImGuiDataType_COUNT: break;
2424
}
2425
IM_ASSERT(0);
2426
return 0;
2427
}
2428
2429
template<typename T>
2430
static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2431
{
2432
// Clamp, both sides are optional, return true if modified
2433
if (v_min && *v < *v_min) { *v = *v_min; return true; }
2434
if (v_max && *v > *v_max) { *v = *v_max; return true; }
2435
return false;
2436
}
2437
2438
bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2439
{
2440
switch (data_type)
2441
{
2442
case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2443
case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2444
case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2445
case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2446
case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2447
case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2448
case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2449
case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2450
case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2451
case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2452
case ImGuiDataType_COUNT: break;
2453
}
2454
IM_ASSERT(0);
2455
return false;
2456
}
2457
2458
bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2459
{
2460
ImGuiContext& g = *GImGui;
2461
return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2462
}
2463
2464
static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2465
{
2466
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 };
2467
if (decimal_precision < 0)
2468
return FLT_MIN;
2469
return (decimal_precision < IM_COUNTOF(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2470
}
2471
2472
template<typename TYPE>
2473
TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2474
{
2475
IM_UNUSED(data_type);
2476
IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2477
const char* fmt_start = ImParseFormatFindStart(format);
2478
if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2479
return v;
2480
2481
// Sanitize format
2482
char fmt_sanitized[32];
2483
ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_COUNTOF(fmt_sanitized));
2484
fmt_start = fmt_sanitized;
2485
2486
// Format value with our rounding, and read back
2487
char v_str[64];
2488
ImFormatString(v_str, IM_COUNTOF(v_str), fmt_start, v);
2489
const char* p = v_str;
2490
while (*p == ' ')
2491
p++;
2492
v = (TYPE)ImAtof(p);
2493
2494
return v;
2495
}
2496
2497
//-------------------------------------------------------------------------
2498
// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2499
//-------------------------------------------------------------------------
2500
// - DragBehaviorT<>() [Internal]
2501
// - DragBehavior() [Internal]
2502
// - DragScalar()
2503
// - DragScalarN()
2504
// - DragFloat()
2505
// - DragFloat2()
2506
// - DragFloat3()
2507
// - DragFloat4()
2508
// - DragFloatRange2()
2509
// - DragInt()
2510
// - DragInt2()
2511
// - DragInt3()
2512
// - DragInt4()
2513
// - DragIntRange2()
2514
//-------------------------------------------------------------------------
2515
2516
// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2517
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2518
bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2519
{
2520
ImGuiContext& g = *GImGui;
2521
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2522
const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2523
const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2524
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2525
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2526
2527
// Default tweak speed
2528
if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2529
v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2530
2531
// Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2532
float adjust_delta = 0.0f;
2533
if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2534
{
2535
adjust_delta = g.IO.MouseDelta[axis];
2536
if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2537
adjust_delta *= 1.0f / 100.0f;
2538
if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks))
2539
adjust_delta *= 10.0f;
2540
}
2541
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2542
{
2543
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2544
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2545
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2546
const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2547
adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2548
v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2549
}
2550
adjust_delta *= v_speed;
2551
2552
// For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2553
if (axis == ImGuiAxis_Y)
2554
adjust_delta = -adjust_delta;
2555
2556
// For logarithmic use our range is effectively 0..1 so scale the delta into that range
2557
if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2558
adjust_delta /= (float)(v_max - v_min);
2559
2560
// Clear current value on activation
2561
// 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.
2562
const bool is_just_activated = g.ActiveIdIsJustActivated;
2563
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));
2564
if (is_just_activated || is_already_past_limits_and_pushing_outward)
2565
{
2566
g.DragCurrentAccum = 0.0f;
2567
g.DragCurrentAccumDirty = false;
2568
}
2569
else if (adjust_delta != 0.0f)
2570
{
2571
g.DragCurrentAccum += adjust_delta;
2572
g.DragCurrentAccumDirty = true;
2573
}
2574
2575
if (!g.DragCurrentAccumDirty)
2576
return false;
2577
2578
TYPE v_cur = *v;
2579
FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2580
2581
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2582
const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2583
if (is_logarithmic)
2584
{
2585
// 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.
2586
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2587
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2588
2589
// Convert to parametric space, apply delta, convert back
2590
float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2591
float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2592
v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2593
v_old_ref_for_accum_remainder = v_old_parametric;
2594
}
2595
else
2596
{
2597
v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2598
}
2599
2600
// Round to user desired precision based on format string
2601
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2602
v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2603
2604
// Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2605
g.DragCurrentAccumDirty = false;
2606
if (is_logarithmic)
2607
{
2608
// Convert to parametric space, apply delta, convert back
2609
float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2610
g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2611
}
2612
else
2613
{
2614
g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2615
}
2616
2617
// Lose zero sign for float/double
2618
if (v_cur == (TYPE)-0)
2619
v_cur = (TYPE)0;
2620
2621
if (*v != v_cur && is_bounded)
2622
{
2623
if (is_wrapped)
2624
{
2625
// Wrap values
2626
if (v_cur < v_min)
2627
v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2628
if (v_cur > v_max)
2629
v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2630
}
2631
else
2632
{
2633
// Clamp values + handle overflow/wrap-around for integer types.
2634
if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2635
v_cur = v_min;
2636
if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2637
v_cur = v_max;
2638
}
2639
}
2640
2641
// Apply result
2642
if (*v == v_cur)
2643
return false;
2644
*v = v_cur;
2645
return true;
2646
}
2647
2648
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)
2649
{
2650
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2651
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.");
2652
2653
ImGuiContext& g = *GImGui;
2654
if (g.ActiveId == id)
2655
{
2656
// Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2657
if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2658
ClearActiveID();
2659
else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2660
ClearActiveID();
2661
}
2662
if (g.ActiveId != id)
2663
return false;
2664
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2665
return false;
2666
2667
switch (data_type)
2668
{
2669
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; }
2670
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; }
2671
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; }
2672
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; }
2673
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);
2674
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);
2675
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);
2676
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);
2677
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);
2678
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);
2679
case ImGuiDataType_COUNT: break;
2680
}
2681
IM_ASSERT(0);
2682
return false;
2683
}
2684
2685
// 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.
2686
// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2687
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)
2688
{
2689
ImGuiWindow* window = GetCurrentWindow();
2690
if (window->SkipItems)
2691
return false;
2692
2693
ImGuiContext& g = *GImGui;
2694
const ImGuiStyle& style = g.Style;
2695
const ImGuiID id = window->GetID(label);
2696
const float w = CalcItemWidth();
2697
const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
2698
2699
const ImVec2 label_size = CalcTextSize(label, NULL, true);
2700
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2701
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));
2702
2703
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2704
ItemSize(total_bb, style.FramePadding.y);
2705
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2706
return false;
2707
2708
// Default format string when passing NULL
2709
if (format == NULL)
2710
format = DataTypeGetInfo(data_type)->PrintFmt;
2711
2712
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2713
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2714
if (!temp_input_is_active)
2715
{
2716
// Tabbing or Ctrl+Click on Drag turns it into an InputText
2717
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2718
const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2719
const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2720
if (make_active && (clicked || double_clicked))
2721
SetKeyOwner(ImGuiKey_MouseLeft, id);
2722
if (make_active && temp_input_allowed)
2723
if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2724
temp_input_is_active = true;
2725
2726
// (Optional) simple click (without moving) turns Drag into an InputText
2727
if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2728
if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2729
{
2730
g.NavActivateId = id;
2731
g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2732
temp_input_is_active = true;
2733
}
2734
2735
// Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
2736
if (make_active)
2737
memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
2738
2739
if (make_active && !temp_input_is_active)
2740
{
2741
SetActiveID(id, window);
2742
SetFocusID(id, window);
2743
FocusWindow(window);
2744
g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2745
}
2746
}
2747
2748
if (temp_input_is_active)
2749
{
2750
// Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2751
bool clamp_enabled = false;
2752
if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2753
{
2754
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
2755
if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2756
clamp_enabled = true;
2757
else if (clamp_range_dir == 0)
2758
clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2759
}
2760
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2761
}
2762
2763
// Draw frame
2764
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2765
RenderNavCursor(frame_bb, id);
2766
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
2767
if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
2768
RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
2769
RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
2770
2771
// Drag behavior
2772
const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2773
if (value_changed)
2774
MarkItemEdited(id);
2775
2776
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2777
char value_buf[64];
2778
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
2779
if (g.LogEnabled)
2780
LogSetNextTextDecoration("{", "}");
2781
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2782
2783
if (label_size.x > 0.0f)
2784
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
2785
2786
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2787
return value_changed;
2788
}
2789
2790
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)
2791
{
2792
ImGuiWindow* window = GetCurrentWindow();
2793
if (window->SkipItems)
2794
return false;
2795
2796
ImGuiContext& g = *GImGui;
2797
bool value_changed = false;
2798
BeginGroup();
2799
PushID(label);
2800
PushMultiItemsWidths(components, CalcItemWidth());
2801
size_t type_size = GDataTypeInfo[data_type].Size;
2802
for (int i = 0; i < components; i++)
2803
{
2804
PushID(i);
2805
if (i > 0)
2806
SameLine(0, g.Style.ItemInnerSpacing.x);
2807
if (flags & ImGuiSliderFlags_ColorMarkers)
2808
SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
2809
value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2810
PopID();
2811
PopItemWidth();
2812
p_data = (void*)((char*)p_data + type_size);
2813
}
2814
PopID();
2815
2816
const char* label_end = FindRenderedTextEnd(label);
2817
if (label != label_end)
2818
{
2819
SameLine(0, g.Style.ItemInnerSpacing.x);
2820
TextEx(label, label_end);
2821
}
2822
2823
EndGroup();
2824
return value_changed;
2825
}
2826
2827
bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2828
{
2829
return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2830
}
2831
2832
bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2833
{
2834
return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2835
}
2836
2837
bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2838
{
2839
return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2840
}
2841
2842
bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2843
{
2844
return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2845
}
2846
2847
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2848
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)
2849
{
2850
ImGuiWindow* window = GetCurrentWindow();
2851
if (window->SkipItems)
2852
return false;
2853
2854
ImGuiContext& g = *GImGui;
2855
PushID(label);
2856
BeginGroup();
2857
PushMultiItemsWidths(2, CalcItemWidth());
2858
2859
float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2860
float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2861
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2862
bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2863
PopItemWidth();
2864
SameLine(0, g.Style.ItemInnerSpacing.x);
2865
2866
float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2867
float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2868
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2869
value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2870
PopItemWidth();
2871
SameLine(0, g.Style.ItemInnerSpacing.x);
2872
2873
TextEx(label, FindRenderedTextEnd(label));
2874
EndGroup();
2875
PopID();
2876
2877
return value_changed;
2878
}
2879
2880
// NB: v_speed is float to allow adjusting the drag speed with more precision
2881
bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2882
{
2883
return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2884
}
2885
2886
bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2887
{
2888
return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2889
}
2890
2891
bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2892
{
2893
return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2894
}
2895
2896
bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2897
{
2898
return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2899
}
2900
2901
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2902
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)
2903
{
2904
ImGuiWindow* window = GetCurrentWindow();
2905
if (window->SkipItems)
2906
return false;
2907
2908
ImGuiContext& g = *GImGui;
2909
PushID(label);
2910
BeginGroup();
2911
PushMultiItemsWidths(2, CalcItemWidth());
2912
2913
int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2914
int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2915
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2916
bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2917
PopItemWidth();
2918
SameLine(0, g.Style.ItemInnerSpacing.x);
2919
2920
int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2921
int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2922
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2923
value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2924
PopItemWidth();
2925
SameLine(0, g.Style.ItemInnerSpacing.x);
2926
2927
TextEx(label, FindRenderedTextEnd(label));
2928
EndGroup();
2929
PopID();
2930
2931
return value_changed;
2932
}
2933
2934
//-------------------------------------------------------------------------
2935
// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2936
//-------------------------------------------------------------------------
2937
// - ScaleRatioFromValueT<> [Internal]
2938
// - ScaleValueFromRatioT<> [Internal]
2939
// - SliderBehaviorT<>() [Internal]
2940
// - SliderBehavior() [Internal]
2941
// - SliderScalar()
2942
// - SliderScalarN()
2943
// - SliderFloat()
2944
// - SliderFloat2()
2945
// - SliderFloat3()
2946
// - SliderFloat4()
2947
// - SliderAngle()
2948
// - SliderInt()
2949
// - SliderInt2()
2950
// - SliderInt3()
2951
// - SliderInt4()
2952
// - VSliderScalar()
2953
// - VSliderFloat()
2954
// - VSliderInt()
2955
//-------------------------------------------------------------------------
2956
2957
// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2958
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2959
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)
2960
{
2961
if (v_min == v_max)
2962
return 0.0f;
2963
IM_UNUSED(data_type);
2964
2965
const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2966
if (is_logarithmic)
2967
{
2968
bool flipped = v_max < v_min;
2969
2970
if (flipped) // Handle the case where the range is backwards
2971
ImSwap(v_min, v_max);
2972
2973
// Fudge min/max to avoid getting close to log(0)
2974
FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2975
FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2976
2977
// Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2978
if ((v_min == 0.0f) && (v_max < 0.0f))
2979
v_min_fudged = -logarithmic_zero_epsilon;
2980
else if ((v_max == 0.0f) && (v_min < 0.0f))
2981
v_max_fudged = -logarithmic_zero_epsilon;
2982
2983
float result;
2984
if (v_clamped <= v_min_fudged)
2985
result = 0.0f; // Workaround for values that are in-range but below our fudge
2986
else if (v_clamped >= v_max_fudged)
2987
result = 1.0f; // Workaround for values that are in-range but above our fudge
2988
else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2989
{
2990
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)
2991
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2992
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2993
if (v == 0.0f)
2994
result = zero_point_center; // Special case for exactly zero
2995
else if (v < 0.0f)
2996
result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2997
else
2998
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));
2999
}
3000
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3001
result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
3002
else
3003
result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
3004
3005
return flipped ? (1.0f - result) : result;
3006
}
3007
else
3008
{
3009
// Linear slider
3010
return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
3011
}
3012
}
3013
3014
// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
3015
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3016
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)
3017
{
3018
// We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
3019
// but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
3020
if (t <= 0.0f || v_min == v_max)
3021
return v_min;
3022
if (t >= 1.0f)
3023
return v_max;
3024
3025
TYPE result = (TYPE)0;
3026
if (is_logarithmic)
3027
{
3028
// Fudge min/max to avoid getting silly results close to zero
3029
FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
3030
FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
3031
3032
const bool flipped = v_max < v_min; // Check if range is "backwards"
3033
if (flipped)
3034
ImSwap(v_min_fudged, v_max_fudged);
3035
3036
// Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
3037
if ((v_max == 0.0f) && (v_min < 0.0f))
3038
v_max_fudged = -logarithmic_zero_epsilon;
3039
3040
float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
3041
3042
if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
3043
{
3044
float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
3045
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
3046
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
3047
if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
3048
result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
3049
else if (t_with_flip < zero_point_center)
3050
result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
3051
else
3052
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))));
3053
}
3054
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
3055
result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
3056
else
3057
result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
3058
}
3059
else
3060
{
3061
// Linear slider
3062
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3063
if (is_floating_point)
3064
{
3065
result = ImLerp(v_min, v_max, t);
3066
}
3067
else if (t < 1.0)
3068
{
3069
// - For integer values we want the clicking position to match the grab box so we round above
3070
// This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
3071
// - 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
3072
// range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
3073
FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
3074
result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
3075
}
3076
}
3077
3078
return result;
3079
}
3080
3081
// FIXME: Try to move more of the code into shared SliderBehavior()
3082
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
3083
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)
3084
{
3085
ImGuiContext& g = *GImGui;
3086
const ImGuiStyle& style = g.Style;
3087
3088
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
3089
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
3090
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
3091
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.
3092
3093
// Calculate bounds
3094
const float grab_padding = 2.0f; // FIXME: Should be part of style.
3095
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
3096
float grab_sz = style.GrabMinSize;
3097
if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
3098
grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
3099
grab_sz = ImMin(grab_sz, slider_sz);
3100
const float slider_usable_sz = slider_sz - grab_sz;
3101
const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
3102
const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3103
3104
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3105
float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3106
if (is_logarithmic)
3107
{
3108
// 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.
3109
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3110
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3111
zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3112
}
3113
3114
// Process interacting with the slider
3115
bool value_changed = false;
3116
if (g.ActiveId == id)
3117
{
3118
bool set_new_value = false;
3119
float clicked_t = 0.0f;
3120
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3121
{
3122
if (!g.IO.MouseDown[0])
3123
{
3124
ClearActiveID();
3125
}
3126
else
3127
{
3128
const float mouse_abs_pos = g.IO.MousePos[axis];
3129
if (g.ActiveIdIsJustActivated)
3130
{
3131
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3132
if (axis == ImGuiAxis_Y)
3133
grab_t = 1.0f - grab_t;
3134
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3135
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.
3136
g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3137
}
3138
if (slider_usable_sz > 0.0f)
3139
clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3140
if (axis == ImGuiAxis_Y)
3141
clicked_t = 1.0f - clicked_t;
3142
set_new_value = true;
3143
}
3144
}
3145
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3146
{
3147
if (g.ActiveIdIsJustActivated)
3148
{
3149
g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3150
g.SliderCurrentAccumDirty = false;
3151
}
3152
3153
float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3154
if (input_delta != 0.0f)
3155
{
3156
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3157
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3158
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3159
if (decimal_precision > 0)
3160
{
3161
input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3162
if (tweak_slow)
3163
input_delta /= 10.0f;
3164
}
3165
else
3166
{
3167
if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3168
input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3169
else
3170
input_delta /= 100.0f;
3171
}
3172
if (tweak_fast)
3173
input_delta *= 10.0f;
3174
3175
g.SliderCurrentAccum += input_delta;
3176
g.SliderCurrentAccumDirty = true;
3177
}
3178
3179
float delta = g.SliderCurrentAccum;
3180
if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3181
{
3182
ClearActiveID();
3183
}
3184
else if (g.SliderCurrentAccumDirty)
3185
{
3186
clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3187
3188
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
3189
{
3190
set_new_value = false;
3191
g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3192
}
3193
else
3194
{
3195
set_new_value = true;
3196
float old_clicked_t = clicked_t;
3197
clicked_t = ImSaturate(clicked_t + delta);
3198
3199
// Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3200
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3201
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3202
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3203
float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3204
3205
if (delta > 0)
3206
g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3207
else
3208
g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3209
}
3210
3211
g.SliderCurrentAccumDirty = false;
3212
}
3213
}
3214
3215
if (set_new_value)
3216
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3217
set_new_value = false;
3218
3219
if (set_new_value)
3220
{
3221
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3222
3223
// Round to user desired precision based on format string
3224
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3225
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3226
3227
// Apply result
3228
if (*v != v_new)
3229
{
3230
*v = v_new;
3231
value_changed = true;
3232
}
3233
}
3234
}
3235
3236
if (slider_sz < 1.0f)
3237
{
3238
*out_grab_bb = ImRect(bb.Min, bb.Min);
3239
}
3240
else
3241
{
3242
// Output grab position so it can be displayed by the caller
3243
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3244
if (axis == ImGuiAxis_Y)
3245
grab_t = 1.0f - grab_t;
3246
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3247
if (axis == ImGuiAxis_X)
3248
*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);
3249
else
3250
*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);
3251
}
3252
3253
return value_changed;
3254
}
3255
3256
// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3257
// 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.
3258
// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3259
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)
3260
{
3261
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3262
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.");
3263
IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3264
3265
switch (data_type)
3266
{
3267
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; }
3268
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; }
3269
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; }
3270
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; }
3271
case ImGuiDataType_S32:
3272
IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3273
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);
3274
case ImGuiDataType_U32:
3275
IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3276
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);
3277
case ImGuiDataType_S64:
3278
IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3279
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);
3280
case ImGuiDataType_U64:
3281
IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3282
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);
3283
case ImGuiDataType_Float:
3284
IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3285
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);
3286
case ImGuiDataType_Double:
3287
IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3288
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);
3289
case ImGuiDataType_COUNT: break;
3290
}
3291
IM_ASSERT(0);
3292
return false;
3293
}
3294
3295
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3296
// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3297
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)
3298
{
3299
ImGuiWindow* window = GetCurrentWindow();
3300
if (window->SkipItems)
3301
return false;
3302
3303
ImGuiContext& g = *GImGui;
3304
const ImGuiStyle& style = g.Style;
3305
const ImGuiID id = window->GetID(label);
3306
const float w = CalcItemWidth();
3307
const ImU32 color_marker = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasColorMarker) ? g.NextItemData.ColorMarker : 0;
3308
3309
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3310
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3311
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));
3312
3313
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3314
ItemSize(total_bb, style.FramePadding.y);
3315
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3316
return false;
3317
3318
// Default format string when passing NULL
3319
if (format == NULL)
3320
format = DataTypeGetInfo(data_type)->PrintFmt;
3321
3322
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3323
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3324
if (!temp_input_is_active)
3325
{
3326
// Tabbing or Ctrl+Click on Slider turns it into an input box
3327
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3328
const bool make_active = (clicked || g.NavActivateId == id);
3329
if (make_active && clicked)
3330
SetKeyOwner(ImGuiKey_MouseLeft, id);
3331
if (make_active && temp_input_allowed)
3332
if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3333
temp_input_is_active = true;
3334
3335
// Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert)
3336
if (make_active)
3337
memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size);
3338
3339
if (make_active && !temp_input_is_active)
3340
{
3341
SetActiveID(id, window);
3342
SetFocusID(id, window);
3343
FocusWindow(window);
3344
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3345
}
3346
}
3347
3348
if (temp_input_is_active)
3349
{
3350
// Only clamp Ctrl+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3351
const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3352
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3353
}
3354
3355
// Draw frame
3356
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3357
RenderNavCursor(frame_bb, id);
3358
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, false, style.FrameRounding);
3359
if (color_marker != 0 && style.ColorMarkerSize > 0.0f)
3360
RenderColorComponentMarker(frame_bb, GetColorU32(color_marker), style.FrameRounding);
3361
RenderFrameBorder(frame_bb.Min, frame_bb.Max, g.Style.FrameRounding);
3362
3363
// Slider behavior
3364
ImRect grab_bb;
3365
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3366
if (value_changed)
3367
MarkItemEdited(id);
3368
3369
// Render grab
3370
if (grab_bb.Max.x > grab_bb.Min.x)
3371
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3372
3373
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3374
char value_buf[64];
3375
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3376
if (g.LogEnabled)
3377
LogSetNextTextDecoration("{", "}");
3378
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3379
3380
if (label_size.x > 0.0f)
3381
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3382
3383
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3384
return value_changed;
3385
}
3386
3387
// Add multiple sliders on 1 line for compact edition of multiple components
3388
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)
3389
{
3390
ImGuiWindow* window = GetCurrentWindow();
3391
if (window->SkipItems)
3392
return false;
3393
3394
ImGuiContext& g = *GImGui;
3395
bool value_changed = false;
3396
BeginGroup();
3397
PushID(label);
3398
PushMultiItemsWidths(components, CalcItemWidth());
3399
size_t type_size = GDataTypeInfo[data_type].Size;
3400
for (int i = 0; i < components; i++)
3401
{
3402
PushID(i);
3403
if (i > 0)
3404
SameLine(0, g.Style.ItemInnerSpacing.x);
3405
if (flags & ImGuiSliderFlags_ColorMarkers)
3406
SetNextItemColorMarker(GDefaultRgbaColorMarkers[i]);
3407
value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3408
PopID();
3409
PopItemWidth();
3410
v = (void*)((char*)v + type_size);
3411
}
3412
PopID();
3413
3414
const char* label_end = FindRenderedTextEnd(label);
3415
if (label != label_end)
3416
{
3417
SameLine(0, g.Style.ItemInnerSpacing.x);
3418
TextEx(label, label_end);
3419
}
3420
3421
EndGroup();
3422
return value_changed;
3423
}
3424
3425
bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3426
{
3427
return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3428
}
3429
3430
bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3431
{
3432
return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3433
}
3434
3435
bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3436
{
3437
return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3438
}
3439
3440
bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3441
{
3442
return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3443
}
3444
3445
bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3446
{
3447
if (format == NULL)
3448
format = "%.0f deg";
3449
float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3450
bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3451
if (value_changed)
3452
*v_rad = v_deg * (2 * IM_PI) / 360.0f;
3453
return value_changed;
3454
}
3455
3456
bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3457
{
3458
return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3459
}
3460
3461
bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3462
{
3463
return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3464
}
3465
3466
bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3467
{
3468
return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3469
}
3470
3471
bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3472
{
3473
return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3474
}
3475
3476
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)
3477
{
3478
ImGuiWindow* window = GetCurrentWindow();
3479
if (window->SkipItems)
3480
return false;
3481
3482
ImGuiContext& g = *GImGui;
3483
const ImGuiStyle& style = g.Style;
3484
const ImGuiID id = window->GetID(label);
3485
3486
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3487
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3488
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));
3489
3490
ItemSize(bb, style.FramePadding.y);
3491
if (!ItemAdd(frame_bb, id))
3492
return false;
3493
3494
// Default format string when passing NULL
3495
if (format == NULL)
3496
format = DataTypeGetInfo(data_type)->PrintFmt;
3497
3498
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3499
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3500
if (clicked || g.NavActivateId == id)
3501
{
3502
if (clicked)
3503
SetKeyOwner(ImGuiKey_MouseLeft, id);
3504
SetActiveID(id, window);
3505
SetFocusID(id, window);
3506
FocusWindow(window);
3507
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3508
}
3509
3510
// Draw frame
3511
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3512
RenderNavCursor(frame_bb, id);
3513
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3514
3515
// Slider behavior
3516
ImRect grab_bb;
3517
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3518
if (value_changed)
3519
MarkItemEdited(id);
3520
3521
// Render grab
3522
if (grab_bb.Max.y > grab_bb.Min.y)
3523
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3524
3525
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3526
// For the vertical slider we allow centered text to overlap the frame padding
3527
char value_buf[64];
3528
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_COUNTOF(value_buf), data_type, p_data, format);
3529
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));
3530
if (label_size.x > 0.0f)
3531
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3532
3533
return value_changed;
3534
}
3535
3536
bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3537
{
3538
return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3539
}
3540
3541
bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3542
{
3543
return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3544
}
3545
3546
//-------------------------------------------------------------------------
3547
// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3548
//-------------------------------------------------------------------------
3549
// - ImParseFormatFindStart() [Internal]
3550
// - ImParseFormatFindEnd() [Internal]
3551
// - ImParseFormatTrimDecorations() [Internal]
3552
// - ImParseFormatSanitizeForPrinting() [Internal]
3553
// - ImParseFormatSanitizeForScanning() [Internal]
3554
// - ImParseFormatPrecision() [Internal]
3555
// - TempInputTextScalar() [Internal]
3556
// - InputScalar()
3557
// - InputScalarN()
3558
// - InputFloat()
3559
// - InputFloat2()
3560
// - InputFloat3()
3561
// - InputFloat4()
3562
// - InputInt()
3563
// - InputInt2()
3564
// - InputInt3()
3565
// - InputInt4()
3566
// - InputDouble()
3567
//-------------------------------------------------------------------------
3568
3569
// We don't use strchr() because our strings are usually very short and often start with '%'
3570
const char* ImParseFormatFindStart(const char* fmt)
3571
{
3572
while (char c = fmt[0])
3573
{
3574
if (c == '%' && fmt[1] != '%')
3575
return fmt;
3576
else if (c == '%')
3577
fmt++;
3578
fmt++;
3579
}
3580
return fmt;
3581
}
3582
3583
const char* ImParseFormatFindEnd(const char* fmt)
3584
{
3585
// Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3586
if (fmt[0] != '%')
3587
return fmt;
3588
const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3589
const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3590
for (char c; (c = *fmt) != 0; fmt++)
3591
{
3592
if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3593
return fmt + 1;
3594
if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3595
return fmt + 1;
3596
}
3597
return fmt;
3598
}
3599
3600
// Extract the format out of a format string with leading or trailing decorations
3601
// fmt = "blah blah" -> return ""
3602
// fmt = "%.3f" -> return fmt
3603
// fmt = "hello %.3f" -> return fmt + 6
3604
// fmt = "%.3f hello" -> return buf written with "%.3f"
3605
const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3606
{
3607
const char* fmt_start = ImParseFormatFindStart(fmt);
3608
if (fmt_start[0] != '%')
3609
return "";
3610
const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3611
if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3612
return fmt_start;
3613
ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3614
return buf;
3615
}
3616
3617
// Sanitize format
3618
// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3619
// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3620
void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3621
{
3622
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3623
IM_UNUSED(fmt_out_size);
3624
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3625
while (fmt_in < fmt_end)
3626
{
3627
char c = *fmt_in++;
3628
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3629
*(fmt_out++) = c;
3630
}
3631
*fmt_out = 0; // Zero-terminate
3632
}
3633
3634
// - 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"
3635
const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3636
{
3637
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3638
const char* fmt_out_begin = fmt_out;
3639
IM_UNUSED(fmt_out_size);
3640
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3641
bool has_type = false;
3642
while (fmt_in < fmt_end)
3643
{
3644
char c = *fmt_in++;
3645
if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3646
continue;
3647
has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3648
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3649
*(fmt_out++) = c;
3650
}
3651
*fmt_out = 0; // Zero-terminate
3652
return fmt_out_begin;
3653
}
3654
3655
template<typename TYPE>
3656
static const char* ImAtoi(const char* src, TYPE* output)
3657
{
3658
int negative = 0;
3659
if (*src == '-') { negative = 1; src++; }
3660
if (*src == '+') { src++; }
3661
TYPE v = 0;
3662
while (*src >= '0' && *src <= '9')
3663
v = (v * 10) + (*src++ - '0');
3664
*output = negative ? -v : v;
3665
return src;
3666
}
3667
3668
// Parse display precision back from the display format string
3669
// 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.
3670
int ImParseFormatPrecision(const char* fmt, int default_precision)
3671
{
3672
fmt = ImParseFormatFindStart(fmt);
3673
if (fmt[0] != '%')
3674
return default_precision;
3675
fmt++;
3676
while (*fmt >= '0' && *fmt <= '9')
3677
fmt++;
3678
int precision = INT_MAX;
3679
if (*fmt == '.')
3680
{
3681
fmt = ImAtoi<int>(fmt + 1, &precision);
3682
if (precision < 0 || precision > 99)
3683
precision = default_precision;
3684
}
3685
if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3686
precision = -1;
3687
if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3688
precision = -1;
3689
return (precision == INT_MAX) ? default_precision : precision;
3690
}
3691
3692
// Create text input in place of another active widget (e.g. used when doing a Ctrl+Click on drag/slider widgets)
3693
// FIXME: Facilitate using this in variety of other situations.
3694
// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3695
// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3696
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3697
{
3698
// On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3699
// We clear ActiveID on the first frame to allow the InputText() taking it back.
3700
ImGuiContext& g = *GImGui;
3701
const bool init = (g.TempInputId != id);
3702
if (init)
3703
ClearActiveID();
3704
3705
g.CurrentWindow->DC.CursorPos = bb.Min;
3706
g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3707
bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem);
3708
if (init)
3709
{
3710
// First frame we started displaying the InputText widget, we expect it to take the active id.
3711
IM_ASSERT(g.ActiveId == id);
3712
g.TempInputId = g.ActiveId;
3713
}
3714
return value_changed;
3715
}
3716
3717
// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3718
// This is intended: this way we allow Ctrl+Click manual input to set a value out of bounds, for maximum flexibility.
3719
// However this may not be ideal for all uses, as some user code may break on out of bound values.
3720
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)
3721
{
3722
// FIXME: May need to clarify display behavior if format doesn't contain %.
3723
// "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3724
ImGuiContext& g = *GImGui;
3725
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3726
char fmt_buf[32];
3727
char data_buf[32];
3728
format = ImParseFormatTrimDecorations(format, fmt_buf, IM_COUNTOF(fmt_buf));
3729
if (format[0] == 0)
3730
format = type_info->PrintFmt;
3731
DataTypeFormatString(data_buf, IM_COUNTOF(data_buf), data_type, p_data, format);
3732
ImStrTrimBlanks(data_buf);
3733
3734
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3735
g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3736
bool value_changed = false;
3737
if (TempInputText(bb, id, label, data_buf, IM_COUNTOF(data_buf), flags))
3738
{
3739
// Backup old value
3740
size_t data_type_size = type_info->Size;
3741
ImGuiDataTypeStorage data_backup;
3742
memcpy(&data_backup, p_data, data_type_size);
3743
3744
// Apply new value (or operations) then clamp
3745
DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3746
if (p_clamp_min || p_clamp_max)
3747
{
3748
if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3749
ImSwap(p_clamp_min, p_clamp_max);
3750
DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3751
}
3752
3753
// Only mark as edited if new value is different
3754
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3755
value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3756
if (value_changed)
3757
MarkItemEdited(id);
3758
}
3759
return value_changed;
3760
}
3761
3762
void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3763
{
3764
ImGuiContext& g = *GImGui;
3765
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3766
memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3767
}
3768
3769
// 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.
3770
// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3771
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)
3772
{
3773
ImGuiWindow* window = GetCurrentWindow();
3774
if (window->SkipItems)
3775
return false;
3776
3777
ImGuiContext& g = *GImGui;
3778
ImGuiStyle& style = g.Style;
3779
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()!
3780
3781
if (format == NULL)
3782
format = DataTypeGetInfo(data_type)->PrintFmt;
3783
3784
void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3785
3786
char buf[64];
3787
if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3788
buf[0] = 0;
3789
else
3790
DataTypeFormatString(buf, IM_COUNTOF(buf), data_type, p_data, format);
3791
3792
// Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3793
// We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3794
g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3795
flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3796
3797
bool value_changed = false;
3798
if (p_step == NULL)
3799
{
3800
if (InputText(label, buf, IM_COUNTOF(buf), flags))
3801
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3802
}
3803
else
3804
{
3805
const float button_size = GetFrameHeight();
3806
3807
BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3808
PushID(label);
3809
SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3810
if (InputText("", buf, IM_COUNTOF(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3811
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3812
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3813
3814
// Step buttons
3815
const ImVec2 backup_frame_padding = style.FramePadding;
3816
style.FramePadding.x = style.FramePadding.y;
3817
if (flags & ImGuiInputTextFlags_ReadOnly)
3818
BeginDisabled();
3819
PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3820
SameLine(0, style.ItemInnerSpacing.x);
3821
if (ButtonEx("-", ImVec2(button_size, button_size)))
3822
{
3823
DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3824
value_changed = true;
3825
}
3826
SameLine(0, style.ItemInnerSpacing.x);
3827
if (ButtonEx("+", ImVec2(button_size, button_size)))
3828
{
3829
DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3830
value_changed = true;
3831
}
3832
PopItemFlag();
3833
if (flags & ImGuiInputTextFlags_ReadOnly)
3834
EndDisabled();
3835
3836
const char* label_end = FindRenderedTextEnd(label);
3837
if (label != label_end)
3838
{
3839
SameLine(0, style.ItemInnerSpacing.x);
3840
TextEx(label, label_end);
3841
}
3842
style.FramePadding = backup_frame_padding;
3843
3844
PopID();
3845
EndGroup();
3846
}
3847
3848
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3849
if (value_changed)
3850
MarkItemEdited(g.LastItemData.ID);
3851
3852
return value_changed;
3853
}
3854
3855
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)
3856
{
3857
ImGuiWindow* window = GetCurrentWindow();
3858
if (window->SkipItems)
3859
return false;
3860
3861
ImGuiContext& g = *GImGui;
3862
bool value_changed = false;
3863
BeginGroup();
3864
PushID(label);
3865
PushMultiItemsWidths(components, CalcItemWidth());
3866
size_t type_size = GDataTypeInfo[data_type].Size;
3867
for (int i = 0; i < components; i++)
3868
{
3869
PushID(i);
3870
if (i > 0)
3871
SameLine(0, g.Style.ItemInnerSpacing.x);
3872
value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3873
PopID();
3874
PopItemWidth();
3875
p_data = (void*)((char*)p_data + type_size);
3876
}
3877
PopID();
3878
3879
const char* label_end = FindRenderedTextEnd(label);
3880
if (label != label_end)
3881
{
3882
SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3883
TextEx(label, label_end);
3884
}
3885
3886
EndGroup();
3887
return value_changed;
3888
}
3889
3890
bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3891
{
3892
return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3893
}
3894
3895
bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3896
{
3897
return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3898
}
3899
3900
bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3901
{
3902
return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3903
}
3904
3905
bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3906
{
3907
return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3908
}
3909
3910
bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3911
{
3912
// 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.
3913
const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3914
return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3915
}
3916
3917
bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3918
{
3919
return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3920
}
3921
3922
bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3923
{
3924
return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3925
}
3926
3927
bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3928
{
3929
return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3930
}
3931
3932
bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3933
{
3934
return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3935
}
3936
3937
//-------------------------------------------------------------------------
3938
// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3939
//-------------------------------------------------------------------------
3940
// - imstb_textedit.h include
3941
// - InputText()
3942
// - InputTextWithHint()
3943
// - InputTextMultiline()
3944
// - InputTextEx() [Internal]
3945
// - DebugNodeInputTextState() [Internal]
3946
//-------------------------------------------------------------------------
3947
3948
namespace ImStb
3949
{
3950
#include "imstb_textedit.h"
3951
}
3952
3953
// If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
3954
bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3955
{
3956
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3957
return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3958
}
3959
3960
bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3961
{
3962
return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3963
}
3964
3965
bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3966
{
3967
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3968
return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3969
}
3970
3971
static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end_display, const char* text_end, const char** out_remaining, ImVec2* out_offset, ImDrawTextFlags flags)
3972
{
3973
ImGuiContext& g = *ctx;
3974
ImGuiInputTextState* obj = &g.InputTextState;
3975
IM_ASSERT(text_end_display >= text_begin && text_end_display <= text_end);
3976
return ImFontCalcTextSizeEx(g.Font, g.FontSize, g.FontWeight, FLT_MAX, obj->WrapWidth, text_begin, text_end_display, text_end, out_remaining, out_offset, flags);
3977
}
3978
3979
// 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)
3980
// With our UTF-8 use of stb_textedit:
3981
// - 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.
3982
// - 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).
3983
// - ...but we don't use that feature.
3984
namespace ImStb
3985
{
3986
static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
3987
static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx >= 0 && idx <= obj->TextLen); return obj->TextSrc[idx]; }
3988
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; }
3989
static char STB_TEXTEDIT_NEWLINE = '\n';
3990
static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3991
{
3992
const char* text = obj->TextSrc;
3993
const char* text_remaining = NULL;
3994
const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, text + obj->TextLen, &text_remaining, NULL, ImDrawTextFlags_StopOnNewLine | ImDrawTextFlags_WrapKeepBlanks);
3995
r->x0 = 0.0f;
3996
r->x1 = size.x;
3997
r->baseline_y_delta = size.y;
3998
r->ymin = 0.0f;
3999
r->ymax = size.y;
4000
r->num_chars = (int)(text_remaining - (text + line_start_idx));
4001
}
4002
4003
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
4004
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
4005
4006
static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4007
{
4008
if (idx >= obj->TextLen)
4009
return obj->TextLen + 1;
4010
unsigned int c;
4011
return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen);
4012
}
4013
4014
static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
4015
{
4016
if (idx <= 0)
4017
return -1;
4018
const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx);
4019
return (int)(p - obj->TextSrc);
4020
}
4021
4022
static bool ImCharIsSeparatorW(unsigned int c)
4023
{
4024
static const unsigned int separator_list[] =
4025
{
4026
',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
4027
'[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
4028
'\n', '\r',
4029
};
4030
for (unsigned int separator : separator_list)
4031
if (c == separator)
4032
return true;
4033
return false;
4034
}
4035
4036
static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
4037
{
4038
// 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.
4039
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4040
return 0;
4041
4042
const char* curr_p = obj->TextSrc + idx;
4043
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4044
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen);
4045
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen);
4046
4047
bool prev_white = ImCharIsBlankW(prev_c);
4048
bool prev_separ = ImCharIsSeparatorW(prev_c);
4049
bool curr_white = ImCharIsBlankW(curr_c);
4050
bool curr_separ = ImCharIsSeparatorW(curr_c);
4051
return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4052
}
4053
static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4054
{
4055
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4056
return 0;
4057
4058
const char* curr_p = obj->TextSrc + idx;
4059
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p);
4060
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen);
4061
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen);
4062
4063
bool prev_white = ImCharIsBlankW(prev_c);
4064
bool prev_separ = ImCharIsSeparatorW(prev_c);
4065
bool curr_white = ImCharIsBlankW(curr_c);
4066
bool curr_separ = ImCharIsSeparatorW(curr_c);
4067
return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4068
}
4069
static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4070
{
4071
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4072
while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4073
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4074
return idx < 0 ? 0 : idx;
4075
}
4076
static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4077
{
4078
int len = obj->TextLen;
4079
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4080
while (idx < len && !is_word_boundary_from_left(obj, idx))
4081
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4082
return idx > len ? len : idx;
4083
}
4084
static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4085
{
4086
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4087
int len = obj->TextLen;
4088
while (idx < len && !is_word_boundary_from_right(obj, idx))
4089
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4090
return idx > len ? len : idx;
4091
}
4092
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); }
4093
#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4094
#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4095
4096
// Reimplementation of stb_textedit_move_line_start()/stb_textedit_move_line_end() which supports word-wrapping.
4097
static int STB_TEXTEDIT_MOVELINESTART_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4098
{
4099
if (state->single_line)
4100
return 0;
4101
4102
if (obj->WrapWidth > 0.0f)
4103
{
4104
ImGuiContext& g = *obj->Ctx;
4105
const char* p_cursor = obj->TextSrc + cursor;
4106
const char* p_bol = ImStrbol(p_cursor, obj->TextSrc);
4107
const char* p = p_bol;
4108
const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4109
while (p >= p_bol)
4110
{
4111
const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, g.FontWeight, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4112
if (p == p_cursor) // If we are already on a visible beginning-of-line, return real beginning-of-line (would be same as regular handler below)
4113
return (int)(p_bol - obj->TextSrc);
4114
if (p_eol == p_cursor && obj->TextA[cursor] != '\n' && obj->LastMoveDirectionLR == ImGuiDir_Left)
4115
return (int)(p_bol - obj->TextSrc);
4116
if (p_eol >= p_cursor)
4117
return (int)(p - obj->TextSrc);
4118
p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4119
}
4120
}
4121
4122
// Regular handler, same as stb_textedit_move_line_start()
4123
while (cursor > 0)
4124
{
4125
int prev_cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, cursor);
4126
if (STB_TEXTEDIT_GETCHAR(obj, prev_cursor) == STB_TEXTEDIT_NEWLINE)
4127
break;
4128
cursor = prev_cursor;
4129
}
4130
return cursor;
4131
}
4132
4133
static int STB_TEXTEDIT_MOVELINEEND_IMPL(ImGuiInputTextState* obj, ImStb::STB_TexteditState* state, int cursor)
4134
{
4135
int n = STB_TEXTEDIT_STRINGLEN(obj);
4136
if (state->single_line)
4137
return n;
4138
4139
if (obj->WrapWidth > 0.0f)
4140
{
4141
ImGuiContext& g = *obj->Ctx;
4142
const char* p_cursor = obj->TextSrc + cursor;
4143
const char* p = ImStrbol(p_cursor, obj->TextSrc);
4144
const char* text_end = obj->TextSrc + obj->TextLen; // End of line would be enough
4145
while (p < text_end)
4146
{
4147
const char* p_eol = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, g.FontWeight, p, text_end, obj->WrapWidth, ImDrawTextFlags_WrapKeepBlanks);
4148
cursor = (int)(p_eol - obj->TextSrc);
4149
if (p_eol == p_cursor && obj->LastMoveDirectionLR != ImGuiDir_Left) // If we are already on a visible end-of-line, switch to regular handle
4150
break;
4151
if (p_eol > p_cursor)
4152
return cursor;
4153
p = (*p_eol == '\n') ? p_eol + 1 : p_eol;
4154
}
4155
}
4156
// Regular handler, same as stb_textedit_move_line_end()
4157
while (cursor < n && STB_TEXTEDIT_GETCHAR(obj, cursor) != STB_TEXTEDIT_NEWLINE)
4158
cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, cursor);
4159
return cursor;
4160
}
4161
4162
#define STB_TEXTEDIT_MOVELINESTART STB_TEXTEDIT_MOVELINESTART_IMPL
4163
#define STB_TEXTEDIT_MOVELINEEND STB_TEXTEDIT_MOVELINEEND_IMPL
4164
4165
static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4166
{
4167
// Offset remaining text (+ copy zero terminator)
4168
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4169
char* dst = obj->TextA.Data + pos;
4170
char* src = obj->TextA.Data + pos + n;
4171
memmove(dst, src, obj->TextLen - n - pos + 1);
4172
obj->Edited = true;
4173
obj->TextLen -= n;
4174
}
4175
4176
static int STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4177
{
4178
const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4179
const int text_len = obj->TextLen;
4180
IM_ASSERT(pos <= text_len);
4181
4182
// We support partial insertion (with a mod in stb_textedit.h)
4183
const int avail = obj->BufCapacity - 1 - obj->TextLen;
4184
if (!is_resizable && new_text_len > avail)
4185
new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4186
if (new_text_len == 0)
4187
return 0;
4188
4189
// Grow internal buffer if needed
4190
IM_ASSERT(obj->TextSrc == obj->TextA.Data);
4191
if (text_len + new_text_len + 1 > obj->TextA.Size && is_resizable)
4192
{
4193
obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4194
obj->TextSrc = obj->TextA.Data;
4195
}
4196
4197
char* text = obj->TextA.Data;
4198
if (pos != text_len)
4199
memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4200
memcpy(text + pos, new_text, (size_t)new_text_len);
4201
4202
obj->Edited = true;
4203
obj->TextLen += new_text_len;
4204
obj->TextA[obj->TextLen] = '\0';
4205
4206
return new_text_len;
4207
}
4208
4209
// 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)
4210
#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4211
#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4212
#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4213
#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4214
#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4215
#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4216
#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4217
#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4218
#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4219
#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4220
#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4221
#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4222
#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4223
#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4224
#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4225
#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4226
#define STB_TEXTEDIT_K_SHIFT 0x400000
4227
4228
#define IMSTB_TEXTEDIT_IMPLEMENTATION
4229
#define IMSTB_TEXTEDIT_memmove memmove
4230
#include "imstb_textedit.h"
4231
4232
// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4233
// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4234
static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4235
{
4236
stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4237
ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4238
state->cursor = state->select_start = state->select_end = 0;
4239
if (text_len <= 0)
4240
return;
4241
int text_len_inserted = ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len);
4242
if (text_len_inserted > 0)
4243
{
4244
state->cursor = state->select_start = state->select_end = text_len;
4245
state->has_preferred_x = 0;
4246
return;
4247
}
4248
IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4249
}
4250
4251
} // namespace ImStb
4252
4253
// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4254
ImGuiInputTextState::ImGuiInputTextState()
4255
{
4256
memset(this, 0, sizeof(*this));
4257
Stb = IM_NEW(ImStbTexteditState);
4258
memset(Stb, 0, sizeof(*Stb));
4259
}
4260
4261
ImGuiInputTextState::~ImGuiInputTextState()
4262
{
4263
IM_DELETE(Stb);
4264
}
4265
4266
void ImGuiInputTextState::OnKeyPressed(int key)
4267
{
4268
stb_textedit_key(this, Stb, key);
4269
CursorFollow = true;
4270
CursorAnimReset();
4271
const int key_u = (key & ~STB_TEXTEDIT_K_SHIFT);
4272
if (key_u == STB_TEXTEDIT_K_LEFT || key_u == STB_TEXTEDIT_K_LINESTART || key_u == STB_TEXTEDIT_K_TEXTSTART || key_u == STB_TEXTEDIT_K_BACKSPACE || key_u == STB_TEXTEDIT_K_WORDLEFT)
4273
LastMoveDirectionLR = ImGuiDir_Left;
4274
else if (key_u == STB_TEXTEDIT_K_RIGHT || key_u == STB_TEXTEDIT_K_LINEEND || key_u == STB_TEXTEDIT_K_TEXTEND || key_u == STB_TEXTEDIT_K_DELETE || key_u == STB_TEXTEDIT_K_WORDRIGHT)
4275
LastMoveDirectionLR = ImGuiDir_Right;
4276
}
4277
4278
void ImGuiInputTextState::OnCharPressed(unsigned int c)
4279
{
4280
// Convert the key to a UTF8 byte sequence.
4281
// The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4282
char utf8[5];
4283
ImTextCharToUtf8(utf8, c);
4284
stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8));
4285
CursorFollow = true;
4286
CursorAnimReset();
4287
}
4288
4289
// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4290
void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4291
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); }
4292
bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4293
void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4294
int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4295
int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4296
int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4297
float ImGuiInputTextState::GetPreferredOffsetX() const { return Stb->has_preferred_x ? Stb->preferred_x : -1; }
4298
void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4299
void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4300
void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4301
void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4302
4303
ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4304
{
4305
memset(this, 0, sizeof(*this));
4306
}
4307
4308
// Public API to manipulate UTF-8 text from within a callback.
4309
// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4310
// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar
4311
// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both.
4312
void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4313
{
4314
IM_ASSERT(pos + bytes_count <= BufTextLen);
4315
char* dst = Buf + pos;
4316
const char* src = Buf + pos + bytes_count;
4317
memmove(dst, src, BufTextLen - bytes_count - pos + 1);
4318
4319
if (CursorPos >= pos + bytes_count)
4320
CursorPos -= bytes_count;
4321
else if (CursorPos >= pos)
4322
CursorPos = pos;
4323
SelectionStart = SelectionEnd = CursorPos;
4324
BufDirty = true;
4325
BufTextLen -= bytes_count;
4326
}
4327
4328
void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4329
{
4330
// Accept null ranges
4331
if (new_text == new_text_end)
4332
return;
4333
4334
ImGuiContext& g = *Ctx;
4335
ImGuiInputTextState* obj = &g.InputTextState;
4336
IM_ASSERT(obj->ID != 0 && g.ActiveId == obj->ID);
4337
const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4338
const bool is_readonly = (Flags & ImGuiInputTextFlags_ReadOnly) != 0;
4339
int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text);
4340
4341
// We support partial insertion (with a mod in stb_textedit.h)
4342
const int avail = BufSize - 1 - BufTextLen;
4343
if (!is_resizable && new_text_len > avail)
4344
new_text_len = (int)(ImTextFindValidUtf8CodepointEnd(new_text, new_text + new_text_len, new_text + avail) - new_text); // Truncate to closest UTF-8 codepoint. Alternative: return 0 to cancel insertion.
4345
if (new_text_len == 0)
4346
return;
4347
4348
// Grow internal buffer if needed
4349
if (new_text_len + BufTextLen + 1 > obj->TextA.Size && is_resizable && !is_readonly)
4350
{
4351
IM_ASSERT(Buf == obj->TextA.Data);
4352
int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4353
obj->TextA.resize(new_buf_size + 1);
4354
obj->TextSrc = obj->TextA.Data;
4355
Buf = obj->TextA.Data;
4356
BufSize = obj->BufCapacity = new_buf_size;
4357
}
4358
4359
if (BufTextLen != pos)
4360
memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4361
memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4362
Buf[BufTextLen + new_text_len] = '\0';
4363
4364
BufDirty = true;
4365
BufTextLen += new_text_len;
4366
if (CursorPos >= pos)
4367
CursorPos += new_text_len;
4368
CursorPos = ImClamp(CursorPos, 0, BufTextLen);
4369
SelectionStart = SelectionEnd = CursorPos;
4370
}
4371
4372
void ImGui::PushPasswordFont()
4373
{
4374
ImGuiContext& g = *GImGui;
4375
ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4376
IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4377
ImFontGlyph* glyph = g.FontBaked->FindGlyph('*');
4378
g.InputTextPasswordFontBackupFlags = g.Font->Flags;
4379
backup->FallbackGlyphIndex = g.FontBaked->FallbackGlyphIndex;
4380
backup->FallbackAdvanceX = g.FontBaked->FallbackAdvanceX;
4381
backup->IndexLookup.swap(g.FontBaked->IndexLookup);
4382
backup->IndexAdvanceX.swap(g.FontBaked->IndexAdvanceX);
4383
g.Font->Flags |= ImFontFlags_NoLoadGlyphs;
4384
g.FontBaked->FallbackGlyphIndex = g.FontBaked->Glyphs.index_from_ptr(glyph);
4385
g.FontBaked->FallbackAdvanceX = glyph->AdvanceX;
4386
}
4387
4388
void ImGui::PopPasswordFont()
4389
{
4390
ImGuiContext& g = *GImGui;
4391
ImFontBaked* backup = &g.InputTextPasswordFontBackupBaked;
4392
g.Font->Flags = g.InputTextPasswordFontBackupFlags;
4393
g.FontBaked->FallbackGlyphIndex = backup->FallbackGlyphIndex;
4394
g.FontBaked->FallbackAdvanceX = backup->FallbackAdvanceX;
4395
g.FontBaked->IndexLookup.swap(backup->IndexLookup);
4396
g.FontBaked->IndexAdvanceX.swap(backup->IndexAdvanceX);
4397
IM_ASSERT(backup->IndexAdvanceX.Size == 0 && backup->IndexLookup.Size == 0);
4398
}
4399
4400
// Return false to discard a character.
4401
static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4402
{
4403
unsigned int c = *p_char;
4404
4405
// Filter non-printable (NB: isprint is unreliable! see #2467)
4406
bool apply_named_filters = true;
4407
if (c < 0x20)
4408
{
4409
bool pass = false;
4410
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)
4411
if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space
4412
{
4413
c = *p_char = ' ';
4414
pass = true;
4415
}
4416
pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0;
4417
pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4418
if (!pass)
4419
return false;
4420
apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4421
}
4422
4423
if (input_source_is_clipboard == false)
4424
{
4425
// We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4426
if (c == 127)
4427
return false;
4428
4429
// Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4430
if (c >= 0xE000 && c <= 0xF8FF)
4431
return false;
4432
}
4433
4434
// Filter Unicode ranges we are not handling in this build
4435
if (c > IM_UNICODE_CODEPOINT_MAX)
4436
return false;
4437
4438
// Generic named filters
4439
if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4440
{
4441
// 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 '.'.
4442
// The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4443
// 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.
4444
// Change the default decimal_point with:
4445
// ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4446
// 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.
4447
ImGuiContext& g = *ctx;
4448
const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4449
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4450
if (c == '.' || c == ',')
4451
c = c_decimal_point;
4452
4453
// Full-width -> half-width conversion for numeric fields: https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4454
// While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4455
// scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4456
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4457
if (c >= 0xFF01 && c <= 0xFF5E)
4458
c = c - 0xFF01 + 0x21;
4459
4460
// Allow 0-9 . - + * /
4461
if (flags & ImGuiInputTextFlags_CharsDecimal)
4462
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4463
return false;
4464
4465
// Allow 0-9 . - + * / e E
4466
if (flags & ImGuiInputTextFlags_CharsScientific)
4467
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4468
return false;
4469
4470
// Allow 0-9 a-F A-F
4471
if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4472
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4473
return false;
4474
4475
// Turn a-z into A-Z
4476
if (flags & ImGuiInputTextFlags_CharsUppercase)
4477
if (c >= 'a' && c <= 'z')
4478
c += (unsigned int)('A' - 'a');
4479
4480
if (flags & ImGuiInputTextFlags_CharsNoBlank)
4481
if (ImCharIsBlankW(c))
4482
return false;
4483
4484
*p_char = c;
4485
}
4486
4487
// Custom callback filter
4488
if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4489
{
4490
ImGuiContext& g = *GImGui;
4491
ImGuiInputTextCallbackData callback_data;
4492
callback_data.Ctx = &g;
4493
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4494
callback_data.EventChar = (ImWchar)c;
4495
callback_data.Flags = flags;
4496
callback_data.UserData = user_data;
4497
if (callback(&callback_data) != 0)
4498
return false;
4499
*p_char = callback_data.EventChar;
4500
if (!callback_data.EventChar)
4501
return false;
4502
}
4503
4504
return true;
4505
}
4506
4507
// Find the shortest single replacement we can make to get from old_buf to new_buf
4508
// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately.
4509
// 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.
4510
static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length)
4511
{
4512
const int shorter_length = ImMin(old_length, new_length);
4513
int first_diff;
4514
for (first_diff = 0; first_diff < shorter_length; first_diff++)
4515
if (old_buf[first_diff] != new_buf[first_diff])
4516
break;
4517
if (first_diff == old_length && first_diff == new_length)
4518
return;
4519
4520
int old_last_diff = old_length - 1;
4521
int new_last_diff = new_length - 1;
4522
for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4523
if (old_buf[old_last_diff] != new_buf[new_last_diff])
4524
break;
4525
4526
const int insert_len = new_last_diff - first_diff + 1;
4527
const int delete_len = old_last_diff - first_diff + 1;
4528
if (insert_len > 0 || delete_len > 0)
4529
if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4530
for (int i = 0; i < delete_len; i++)
4531
p[i] = old_buf[first_diff + i];
4532
}
4533
4534
// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4535
// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4536
// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4537
// but that more likely be attractive when we do have _NoLiveEdit flag available.
4538
void ImGui::InputTextDeactivateHook(ImGuiID id)
4539
{
4540
ImGuiContext& g = *GImGui;
4541
ImGuiInputTextState* state = &g.InputTextState;
4542
if (id == 0 || state->ID != id)
4543
return;
4544
g.InputTextDeactivatedState.ID = state->ID;
4545
if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4546
{
4547
g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4548
}
4549
else
4550
{
4551
IM_ASSERT(state->TextA.Data != 0);
4552
IM_ASSERT(state->TextA[state->TextLen] == 0);
4553
g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4554
memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4555
}
4556
}
4557
4558
static int* ImLowerBound(int* in_begin, int* in_end, int v)
4559
{
4560
int* in_p = in_begin;
4561
for (size_t count = (size_t)(in_end - in_p); count > 0; )
4562
{
4563
size_t count2 = count >> 1;
4564
int* mid = in_p + count2;
4565
if (*mid < v)
4566
{
4567
in_p = ++mid;
4568
count -= count2 + 1;
4569
}
4570
else
4571
{
4572
count = count2;
4573
}
4574
}
4575
return in_p;
4576
}
4577
4578
// FIXME-WORDWRAP: Bundle some of this into ImGuiTextIndex and/or extract as a different tool?
4579
// 'max_output_buffer_size' happens to be a meaningful optimization to avoid writing the full line_index when not necessarily needed (e.g. very large buffer, scrolled up, inactive)
4580
static int InputTextLineIndexBuild(ImGuiInputTextFlags flags, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, float wrap_width, int max_output_buffer_size, const char** out_buf_end)
4581
{
4582
ImGuiContext& g = *GImGui;
4583
int size = 0;
4584
const char* s;
4585
if (flags & ImGuiInputTextFlags_WordWrap)
4586
{
4587
for (s = buf; s < buf_end; s = (*s == '\n') ? s + 1 : s)
4588
{
4589
if (size++ <= max_output_buffer_size)
4590
line_index->Offsets.push_back((int)(s - buf));
4591
s = ImFontCalcWordWrapPositionEx(g.Font, g.FontSize, g.FontWeight, s, buf_end, wrap_width, ImDrawTextFlags_WrapKeepBlanks);
4592
}
4593
}
4594
else if (buf_end != NULL)
4595
{
4596
for (s = buf; s < buf_end; s = s ? s + 1 : buf_end)
4597
{
4598
if (size++ <= max_output_buffer_size)
4599
line_index->Offsets.push_back((int)(s - buf));
4600
s = (const char*)ImMemchr(s, '\n', buf_end - s);
4601
}
4602
}
4603
else
4604
{
4605
const char* s_eol;
4606
for (s = buf; ; s = s_eol + 1)
4607
{
4608
if (size++ <= max_output_buffer_size)
4609
line_index->Offsets.push_back((int)(s - buf));
4610
if ((s_eol = strchr(s, '\n')) != NULL)
4611
continue;
4612
s += strlen(s);
4613
break;
4614
}
4615
}
4616
if (out_buf_end != NULL)
4617
*out_buf_end = buf_end = s;
4618
if (size == 0)
4619
{
4620
line_index->Offsets.push_back(0);
4621
size++;
4622
}
4623
if (buf_end > buf && buf_end[-1] == '\n' && size <= max_output_buffer_size)
4624
{
4625
line_index->Offsets.push_back((int)(buf_end - buf));
4626
size++;
4627
}
4628
return size;
4629
}
4630
4631
static ImVec2 InputTextLineIndexGetPosOffset(ImGuiContext& g, ImGuiInputTextState* state, ImGuiTextIndex* line_index, const char* buf, const char* buf_end, int cursor_n)
4632
{
4633
const char* cursor_ptr = buf + cursor_n;
4634
int* it_begin = line_index->Offsets.begin();
4635
int* it_end = line_index->Offsets.end();
4636
const int* it = ImLowerBound(it_begin, it_end, cursor_n);
4637
if (it > it_begin)
4638
if (it == it_end || *it != cursor_n || (state != NULL && state->WrapWidth > 0.0f && state->LastMoveDirectionLR == ImGuiDir_Right && cursor_ptr[-1] != '\n' && cursor_ptr[-1] != 0))
4639
it--;
4640
4641
const int line_no = (it == it_begin) ? 0 : line_index->Offsets.index_from_ptr(it);
4642
const char* line_start = line_index->get_line_begin(buf, line_no);
4643
ImVec2 offset;
4644
offset.x = InputTextCalcTextSize(&g, line_start, cursor_ptr, buf_end, NULL, NULL, ImDrawTextFlags_WrapKeepBlanks).x;
4645
offset.y = (line_no + 1) * g.FontSize;
4646
return offset;
4647
}
4648
4649
// Edit a string of text
4650
// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4651
// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4652
// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4653
// - 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.
4654
// - If you want to use InputText() with std::string or any custom dynamic string type, use the wrapper in misc/cpp/imgui_stdlib.h/.cpp!
4655
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)
4656
{
4657
ImGuiWindow* window = GetCurrentWindow();
4658
if (window->SkipItems)
4659
return false;
4660
4661
IM_ASSERT(buf != NULL && buf_size >= 0);
4662
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4663
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4664
IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline does not not work with left-trimming
4665
IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Password) == 0); // WordWrap does not work with Password mode.
4666
IM_ASSERT((flags & ImGuiInputTextFlags_WordWrap) == 0 || (flags & ImGuiInputTextFlags_Multiline) != 0); // WordWrap does not work in single-line mode.
4667
4668
ImGuiContext& g = *GImGui;
4669
ImGuiIO& io = g.IO;
4670
const ImGuiStyle& style = g.Style;
4671
4672
const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4673
const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4674
4675
if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4676
BeginGroup();
4677
const ImGuiID id = window->GetID(label);
4678
const ImVec2 label_size = CalcTextSize(label, NULL, true);
4679
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
4680
const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4681
4682
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4683
const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4684
4685
ImGuiWindow* draw_window = window;
4686
ImVec2 inner_size = frame_size;
4687
ImGuiLastItemData item_data_backup;
4688
if (is_multiline)
4689
{
4690
ImVec2 backup_pos = window->DC.CursorPos;
4691
ItemSize(total_bb, style.FramePadding.y);
4692
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4693
{
4694
EndGroup();
4695
return false;
4696
}
4697
item_data_backup = g.LastItemData;
4698
window->DC.CursorPos = backup_pos;
4699
4700
// Prevent NavActivation from explicit Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4701
if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && !(g.NavActivateFlags & ImGuiActivateFlags_FromFocusApi) && (flags & ImGuiInputTextFlags_AllowTabInput))
4702
g.NavActivateId = 0;
4703
4704
// Prevent NavActivate reactivating in BeginChild() when we are already active.
4705
const ImGuiID backup_activate_id = g.NavActivateId;
4706
if (g.ActiveId == id) // Prevent reactivation
4707
g.NavActivateId = 0;
4708
4709
// We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4710
PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4711
PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4712
PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4713
PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4714
bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4715
g.NavActivateId = backup_activate_id;
4716
PopStyleVar(3);
4717
PopStyleColor();
4718
if (!child_visible)
4719
{
4720
EndChild();
4721
EndGroup();
4722
return false;
4723
}
4724
draw_window = g.CurrentWindow; // Child window
4725
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.
4726
draw_window->DC.CursorPos += style.FramePadding;
4727
inner_size.x -= draw_window->ScrollbarSizes.x;
4728
}
4729
else
4730
{
4731
// Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4732
ItemSize(total_bb, style.FramePadding.y);
4733
if (!(flags & ImGuiInputTextFlags_MergedItem))
4734
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4735
return false;
4736
}
4737
4738
// Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4739
bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4740
if (hovered)
4741
SetMouseCursor(ImGuiMouseCursor_TextInput);
4742
if (hovered && g.NavHighlightItemUnderNav)
4743
hovered = false;
4744
4745
// We are only allowed to access the state if we are already the active widget.
4746
ImGuiInputTextState* state = GetInputTextState(id);
4747
4748
if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4749
flags |= ImGuiInputTextFlags_ReadOnly;
4750
const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4751
const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4752
const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4753
const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4754
if (is_resizable)
4755
IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4756
4757
// Word-wrapping: enforcing a fixed width not altered by vertical scrollbar makes things easier, notably to track cursor reliably and avoid one-frame glitches.
4758
// Instead of using ImGuiWindowFlags_AlwaysVerticalScrollbar we account for that space if the scrollbar is not visible.
4759
const bool is_wordwrap = (flags & ImGuiInputTextFlags_WordWrap) != 0;
4760
float wrap_width = 0.0f;
4761
if (is_wordwrap)
4762
wrap_width = ImMax(1.0f, GetContentRegionAvail().x + (draw_window->ScrollbarY ? 0.0f : -g.Style.ScrollbarSize));
4763
4764
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard || g.NavInputSource == ImGuiInputSource_Gamepad)));
4765
4766
const bool user_clicked = hovered && io.MouseClicked[0];
4767
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4768
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4769
bool clear_active_id = false;
4770
bool select_all = false;
4771
4772
float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4773
4774
const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf);
4775
const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4776
const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4777
const bool init_state = (init_make_active || user_scroll_active);
4778
if (init_reload_from_user_buf)
4779
{
4780
int new_len = (int)ImStrlen(buf);
4781
IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?");
4782
state->WantReloadUserBuf = false;
4783
InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len);
4784
state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4785
state->TextLen = new_len;
4786
memcpy(state->TextA.Data, buf, state->TextLen + 1);
4787
state->Stb->select_start = state->ReloadSelectionStart;
4788
state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; // will be clamped to bounds below
4789
}
4790
else if ((init_state && g.ActiveId != id) || init_changed_specs)
4791
{
4792
// Access state even if we don't own it yet.
4793
state = &g.InputTextState;
4794
state->CursorAnimReset();
4795
4796
// 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)
4797
InputTextDeactivateHook(state->ID);
4798
4799
// Take a copy of the initial buffer value.
4800
// From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4801
const int buf_len = (int)ImStrlen(buf);
4802
IM_ASSERT(((buf_len + 1 <= buf_size) || (buf_len == 0 && buf_size == 0)) && "Is your input buffer properly zero-terminated?");
4803
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.
4804
memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4805
4806
// Preserve cursor position and undo/redo stack if we come back to same widget
4807
// FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4808
bool recycle_state = (state->ID == id && !init_changed_specs);
4809
if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0)))
4810
recycle_state = false;
4811
4812
// Start edition
4813
state->ID = id;
4814
state->TextLen = buf_len;
4815
if (!is_readonly)
4816
{
4817
state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4818
memcpy(state->TextA.Data, buf, state->TextLen + 1);
4819
}
4820
4821
// Find initial scroll position for right alignment
4822
state->Scroll = ImVec2(0.0f, 0.0f);
4823
if (flags & ImGuiInputTextFlags_ElideLeft)
4824
state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4825
4826
// Recycle existing cursor/selection/undo stack but clamp position
4827
// Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4828
if (!recycle_state)
4829
stb_textedit_initialize_state(state->Stb, !is_multiline);
4830
4831
if (!is_multiline)
4832
{
4833
if (flags & ImGuiInputTextFlags_AutoSelectAll)
4834
select_all = true;
4835
if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4836
select_all = true;
4837
if (user_clicked && io.KeyCtrl)
4838
select_all = true;
4839
}
4840
4841
if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4842
state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4843
}
4844
4845
const bool is_osx = io.ConfigMacOSXBehaviors;
4846
if (g.ActiveId != id && init_make_active)
4847
{
4848
IM_ASSERT(state && state->ID == id);
4849
SetActiveID(id, window);
4850
SetFocusID(id, window);
4851
FocusWindow(window);
4852
}
4853
if (g.ActiveId == id)
4854
{
4855
// Declare some inputs, the other are registered and polled via Shortcut() routing system.
4856
// 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.
4857
const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4858
for (ImGuiKey key : always_owned_keys)
4859
SetKeyOwner(key, id);
4860
if (user_clicked)
4861
SetKeyOwner(ImGuiKey_MouseLeft, id);
4862
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4863
if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4864
{
4865
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4866
SetKeyOwner(ImGuiKey_UpArrow, id);
4867
SetKeyOwner(ImGuiKey_DownArrow, id);
4868
}
4869
if (is_multiline)
4870
{
4871
SetKeyOwner(ImGuiKey_PageUp, id);
4872
SetKeyOwner(ImGuiKey_PageDown, id);
4873
}
4874
// FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4875
if (is_osx)
4876
SetKeyOwner(ImGuiMod_Alt, id);
4877
4878
// Expose scroll in a manner that is agnostic to us using a child window
4879
if (is_multiline && state != NULL)
4880
state->Scroll.y = draw_window->Scroll.y;
4881
4882
// Read-only mode always ever read from source buffer. Refresh TextLen when active.
4883
if (is_readonly && state != NULL)
4884
state->TextLen = (int)ImStrlen(buf);
4885
if (state != NULL)
4886
state->CursorClamp();
4887
//if (is_readonly && state != NULL)
4888
// state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation.
4889
}
4890
if (state != NULL)
4891
state->TextSrc = is_readonly ? buf : state->TextA.Data;
4892
4893
// 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)
4894
if (g.ActiveId == id && state == NULL)
4895
ClearActiveID();
4896
4897
// Release focus when we click outside
4898
if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4899
clear_active_id = true;
4900
4901
// Lock the decision of whether we are going to take the path displaying the cursor or selection
4902
bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4903
bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4904
bool value_changed = false;
4905
bool validated = false;
4906
4907
// Select the buffer to render.
4908
const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4909
bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4910
4911
// Password pushes a temporary font with only a fallback glyph
4912
if (is_password && !is_displaying_hint)
4913
PushPasswordFont();
4914
4915
// Word-wrapping: attempt to keep cursor in view while resizing frame/parent
4916
// FIXME-WORDWRAP: It would be better to preserve same relative offset.
4917
if (is_wordwrap && state != NULL && state->ID == id && state->WrapWidth != wrap_width)
4918
{
4919
state->CursorCenterY = true;
4920
state->WrapWidth = wrap_width;
4921
render_cursor = true;
4922
}
4923
4924
// Process mouse inputs and character inputs
4925
if (g.ActiveId == id)
4926
{
4927
IM_ASSERT(state != NULL);
4928
state->Edited = false;
4929
state->BufCapacity = buf_size;
4930
state->Flags = flags;
4931
state->WrapWidth = wrap_width;
4932
4933
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4934
// Down the line we should have a cleaner library-wide concept of Selected vs Active.
4935
g.ActiveIdAllowOverlap = !io.MouseDown[0];
4936
4937
// Edit in progress
4938
const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4939
const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4940
4941
if (select_all)
4942
{
4943
state->SelectAll();
4944
state->SelectedAllMouseLock = true;
4945
}
4946
else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4947
{
4948
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4949
const int multiclick_count = (io.MouseClickedCount[0] - 2);
4950
if ((multiclick_count % 2) == 0)
4951
{
4952
// Double-click: Select word
4953
// We always use the "Mac" word advance for double-click select vs Ctrl+Right which use the platform dependent variant:
4954
// FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4955
const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
4956
if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4957
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4958
//state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4959
if (!STB_TEXT_HAS_SELECTION(state->Stb))
4960
ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
4961
state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
4962
state->Stb->select_end = state->Stb->cursor;
4963
ImStb::stb_textedit_clamp(state, state->Stb);
4964
}
4965
else
4966
{
4967
// Triple-click: Select line
4968
const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
4969
state->WrapWidth = 0.0f; // Temporarily disable wrapping so we use real line start.
4970
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4971
state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4972
state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4973
state->WrapWidth = wrap_width;
4974
if (!is_eol && is_multiline)
4975
{
4976
ImSwap(state->Stb->select_start, state->Stb->select_end);
4977
state->Stb->cursor = state->Stb->select_end;
4978
}
4979
state->CursorFollow = false;
4980
}
4981
state->CursorAnimReset();
4982
}
4983
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4984
{
4985
if (hovered)
4986
{
4987
if (io.KeyShift)
4988
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4989
else
4990
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4991
state->CursorAnimReset();
4992
}
4993
}
4994
else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4995
{
4996
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4997
state->CursorAnimReset();
4998
state->CursorFollow = true;
4999
}
5000
if (state->SelectedAllMouseLock && !io.MouseDown[0])
5001
state->SelectedAllMouseLock = false;
5002
5003
// We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
5004
// (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)
5005
if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
5006
{
5007
if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
5008
{
5009
unsigned int c = '\t'; // Insert TAB
5010
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
5011
state->OnCharPressed(c);
5012
}
5013
// FIXME: Implement Shift+Tab
5014
/*
5015
if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
5016
{
5017
}
5018
*/
5019
}
5020
5021
// Process regular text input (before we check for Return because using some IME will effectively send a Return?)
5022
// 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.
5023
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
5024
if (io.InputQueueCharacters.Size > 0)
5025
{
5026
if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
5027
for (int n = 0; n < io.InputQueueCharacters.Size; n++)
5028
{
5029
// Insert character if they pass filtering
5030
unsigned int c = (unsigned int)io.InputQueueCharacters[n];
5031
if (c == '\t') // Skip Tab, see above.
5032
continue;
5033
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
5034
state->OnCharPressed(c);
5035
}
5036
5037
// Consume characters
5038
io.InputQueueCharacters.resize(0);
5039
}
5040
}
5041
5042
// Process other shortcuts/key-presses
5043
bool revert_edit = false;
5044
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
5045
{
5046
IM_ASSERT(state != NULL);
5047
5048
const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
5049
state->Stb->row_count_per_page = row_count_per_page;
5050
5051
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
5052
const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
5053
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
5054
5055
// 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)
5056
// Otherwise we could simply assume that we own the keys as we are active.
5057
const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
5058
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());
5059
const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
5060
const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
5061
const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
5062
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;
5063
const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
5064
5065
// 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.
5066
const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
5067
const bool is_enter = Shortcut(ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiKey_KeypadEnter, f_repeat, id);
5068
const bool is_ctrl_enter = Shortcut(ImGuiMod_Ctrl | ImGuiKey_Enter, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_KeypadEnter, f_repeat, id);
5069
const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
5070
const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
5071
5072
// FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
5073
// FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
5074
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); }
5075
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); }
5076
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); }
5077
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); }
5078
else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
5079
else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
5080
else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
5081
else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
5082
else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
5083
{
5084
if (!state->HasSelection())
5085
{
5086
// 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)
5087
if (is_wordmove_key_down)
5088
state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
5089
}
5090
state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
5091
}
5092
else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
5093
{
5094
if (!state->HasSelection())
5095
{
5096
if (is_wordmove_key_down)
5097
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
5098
else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
5099
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
5100
}
5101
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
5102
}
5103
else if (is_enter || is_ctrl_enter || is_gamepad_validate)
5104
{
5105
// Determine if we turn Enter into a \n character
5106
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
5107
if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line != is_ctrl_enter))
5108
{
5109
validated = true;
5110
if (io.ConfigInputTextEnterKeepActive && !is_multiline)
5111
state->SelectAll(); // No need to scroll
5112
else
5113
clear_active_id = true;
5114
}
5115
else if (!is_readonly)
5116
{
5117
// Insert new line
5118
unsigned int c = '\n';
5119
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
5120
state->OnCharPressed(c);
5121
}
5122
}
5123
else if (is_cancel)
5124
{
5125
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5126
{
5127
if (state->TextA.Data[0] != 0)
5128
{
5129
revert_edit = true;
5130
}
5131
else
5132
{
5133
render_cursor = render_selection = false;
5134
clear_active_id = true;
5135
}
5136
}
5137
else
5138
{
5139
clear_active_id = revert_edit = true;
5140
render_cursor = render_selection = false;
5141
}
5142
}
5143
else if (is_undo || is_redo)
5144
{
5145
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
5146
state->ClearSelection();
5147
}
5148
else if (is_select_all)
5149
{
5150
state->SelectAll();
5151
state->CursorFollow = true;
5152
}
5153
else if (is_cut || is_copy)
5154
{
5155
// Cut, Copy
5156
if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
5157
{
5158
// SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy.
5159
const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
5160
const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
5161
g.TempBuffer.reserve(ie - ib + 1);
5162
memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib);
5163
g.TempBuffer.Data[ie - ib] = 0;
5164
SetClipboardText(g.TempBuffer.Data);
5165
}
5166
if (is_cut)
5167
{
5168
if (!state->HasSelection())
5169
state->SelectAll();
5170
state->CursorFollow = true;
5171
stb_textedit_cut(state, state->Stb);
5172
}
5173
}
5174
else if (is_paste)
5175
{
5176
if (const char* clipboard = GetClipboardText())
5177
{
5178
// Filter pasted buffer
5179
const int clipboard_len = (int)ImStrlen(clipboard);
5180
const char* clipboard_end = clipboard + clipboard_len;
5181
ImVector<char> clipboard_filtered;
5182
clipboard_filtered.reserve(clipboard_len + 1);
5183
for (const char* s = clipboard; *s != 0; )
5184
{
5185
unsigned int c;
5186
int in_len = ImTextCharFromUtf8(&c, s, clipboard_end);
5187
s += in_len;
5188
if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true))
5189
continue;
5190
char c_utf8[5];
5191
ImTextCharToUtf8(c_utf8, c);
5192
int out_len = (int)ImStrlen(c_utf8);
5193
clipboard_filtered.resize(clipboard_filtered.Size + out_len);
5194
memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len);
5195
}
5196
if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation
5197
{
5198
clipboard_filtered.push_back(0);
5199
stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1);
5200
state->CursorFollow = true;
5201
}
5202
}
5203
}
5204
5205
// Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
5206
render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
5207
}
5208
5209
// Process callbacks and apply result back to user's buffer.
5210
const char* apply_new_text = NULL;
5211
int apply_new_text_length = 0;
5212
if (g.ActiveId == id)
5213
{
5214
IM_ASSERT(state != NULL);
5215
if (revert_edit && !is_readonly)
5216
{
5217
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
5218
{
5219
// Clear input
5220
IM_ASSERT(state->TextA.Data[0] != 0);
5221
apply_new_text = "";
5222
apply_new_text_length = 0;
5223
value_changed = true;
5224
char empty_string = 0;
5225
stb_textedit_replace(state, state->Stb, &empty_string, 0);
5226
}
5227
else if (strcmp(state->TextA.Data, state->TextToRevertTo.Data) != 0)
5228
{
5229
apply_new_text = state->TextToRevertTo.Data;
5230
apply_new_text_length = state->TextToRevertTo.Size - 1;
5231
5232
// Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
5233
// Push records into the undo stack so we can Ctrl+Z the revert operation itself
5234
value_changed = true;
5235
stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
5236
}
5237
}
5238
5239
// FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId,
5240
// even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks.
5241
// If we do that, need to ensure that as special case, 'validated == true' also writes back.
5242
// This also allows the user to use InputText() without maintaining any user-side storage.
5243
// (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
5244
// unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
5245
const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
5246
if (apply_edit_back_to_user_buffer)
5247
{
5248
// Apply current edited text immediately.
5249
// Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
5250
5251
// User callback
5252
if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
5253
{
5254
IM_ASSERT(callback != NULL);
5255
5256
// The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
5257
ImGuiInputTextFlags event_flag = 0;
5258
ImGuiKey event_key = ImGuiKey_None;
5259
if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
5260
{
5261
event_flag = ImGuiInputTextFlags_CallbackCompletion;
5262
event_key = ImGuiKey_Tab;
5263
}
5264
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
5265
{
5266
event_flag = ImGuiInputTextFlags_CallbackHistory;
5267
event_key = ImGuiKey_UpArrow;
5268
}
5269
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
5270
{
5271
event_flag = ImGuiInputTextFlags_CallbackHistory;
5272
event_key = ImGuiKey_DownArrow;
5273
}
5274
else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5275
{
5276
event_flag = ImGuiInputTextFlags_CallbackEdit;
5277
}
5278
else if (flags & ImGuiInputTextFlags_CallbackAlways)
5279
{
5280
event_flag = ImGuiInputTextFlags_CallbackAlways;
5281
}
5282
5283
if (event_flag)
5284
{
5285
ImGuiInputTextCallbackData callback_data;
5286
callback_data.Ctx = &g;
5287
callback_data.EventFlag = event_flag;
5288
callback_data.Flags = flags;
5289
callback_data.UserData = callback_user_data;
5290
5291
// FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5292
char* callback_buf = is_readonly ? buf : state->TextA.Data;
5293
IM_ASSERT(callback_buf == state->TextSrc);
5294
state->CallbackTextBackup.resize(state->TextLen + 1);
5295
memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1);
5296
5297
callback_data.EventKey = event_key;
5298
callback_data.Buf = callback_buf;
5299
callback_data.BufTextLen = state->TextLen;
5300
callback_data.BufSize = state->BufCapacity;
5301
callback_data.BufDirty = false;
5302
callback_data.CursorPos = state->Stb->cursor;
5303
callback_data.SelectionStart = state->Stb->select_start;
5304
callback_data.SelectionEnd = state->Stb->select_end;
5305
5306
// Call user code
5307
callback(&callback_data);
5308
5309
// Read back what user may have modified
5310
callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5311
IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5312
IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5313
IM_ASSERT(callback_data.Flags == flags);
5314
if (callback_data.BufDirty || callback_data.CursorPos != state->Stb->cursor)
5315
state->CursorFollow = true;
5316
state->Stb->cursor = ImClamp(callback_data.CursorPos, 0, callback_data.BufTextLen);
5317
state->Stb->select_start = ImClamp(callback_data.SelectionStart, 0, callback_data.BufTextLen);
5318
state->Stb->select_end = ImClamp(callback_data.SelectionEnd, 0, callback_data.BufTextLen);
5319
if (callback_data.BufDirty)
5320
{
5321
// Callback may update buffer and thus set buf_dirty even in read-only mode.
5322
IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5323
InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen);
5324
state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5325
state->CursorAnimReset();
5326
}
5327
}
5328
}
5329
5330
// Will copy result string if modified
5331
if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
5332
{
5333
apply_new_text = state->TextSrc;
5334
apply_new_text_length = state->TextLen;
5335
value_changed = true;
5336
}
5337
}
5338
}
5339
5340
// Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5341
if (g.InputTextDeactivatedState.ID == id)
5342
{
5343
if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5344
{
5345
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5346
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5347
value_changed = true;
5348
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5349
}
5350
g.InputTextDeactivatedState.ID = 0;
5351
}
5352
5353
// Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5354
if (apply_new_text != NULL)
5355
{
5356
//// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
5357
//// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
5358
//// without any storage on user's side.
5359
IM_ASSERT(apply_new_text_length >= 0);
5360
if (is_resizable)
5361
{
5362
ImGuiInputTextCallbackData callback_data;
5363
callback_data.Ctx = &g;
5364
callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5365
callback_data.Flags = flags;
5366
callback_data.Buf = buf;
5367
callback_data.BufTextLen = apply_new_text_length;
5368
callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5369
callback_data.UserData = callback_user_data;
5370
callback(&callback_data);
5371
buf = callback_data.Buf;
5372
buf_size = callback_data.BufSize;
5373
apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5374
IM_ASSERT(apply_new_text_length <= buf_size);
5375
}
5376
//IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5377
5378
// If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5379
ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5380
}
5381
5382
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5383
// Otherwise request text input ahead for next frame.
5384
if (g.ActiveId == id && clear_active_id)
5385
ClearActiveID();
5386
5387
// Render frame
5388
if (!is_multiline)
5389
{
5390
RenderNavCursor(frame_bb, id);
5391
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5392
}
5393
5394
ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5395
ImVec2 text_size(0.0f, 0.0f);
5396
ImRect clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
5397
if (is_multiline)
5398
clip_rect.ClipWith(draw_window->ClipRect);
5399
5400
// Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5401
// without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5402
// Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5403
const int buf_display_max_length = 2 * 1024 * 1024;
5404
const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5405
const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5406
5407
// Display hint when contents is empty
5408
// At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368)
5409
const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
5410
if (new_is_displaying_hint != is_displaying_hint)
5411
{
5412
if (is_password && !is_displaying_hint)
5413
PopPasswordFont();
5414
is_displaying_hint = new_is_displaying_hint;
5415
if (is_password && !is_displaying_hint)
5416
PushPasswordFont();
5417
}
5418
if (is_displaying_hint)
5419
{
5420
buf_display = hint;
5421
buf_display_end = hint + ImStrlen(hint);
5422
}
5423
else
5424
{
5425
if (render_cursor || render_selection || g.ActiveId == id)
5426
buf_display_end = buf_display + state->TextLen; //-V595
5427
else if (is_multiline && !is_wordwrap)
5428
buf_display_end = NULL; // Inactive multi-line: end of buffer will be output by InputTextLineIndexBuild() special strchr() path.
5429
else
5430
buf_display_end = buf_display + ImStrlen(buf_display);
5431
}
5432
5433
// Calculate visibility
5434
int line_visible_n0 = 0, line_visible_n1 = 1;
5435
if (is_multiline)
5436
CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5437
5438
// Build line index for easy data access (makes code below simpler and faster)
5439
ImGuiTextIndex* line_index = &g.InputTextLineIndex;
5440
line_index->Offsets.resize(0);
5441
int line_count = 1;
5442
if (is_multiline)
5443
{
5444
// If scrolling is expected to change build full index.
5445
// FIXME-OPT: Could append to index when new value of line_visible_n1 becomes bigger, see second call to CalcClipRectVisibleItemsY() below.
5446
bool will_scroll_y = state && ((state->CursorFollow && render_cursor) || (state->CursorCenterY && (render_cursor || render_selection)));
5447
line_count = InputTextLineIndexBuild(flags, line_index, buf_display, buf_display_end, wrap_width, will_scroll_y ? INT_MAX : line_visible_n1 + 1, buf_display_end ? NULL : &buf_display_end);
5448
}
5449
line_index->EndOffset = (int)(buf_display_end - buf_display);
5450
line_visible_n1 = ImMin(line_visible_n1, line_count);
5451
5452
// Store text height (we don't need width)
5453
text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5454
//GetForegroundDrawList()->AddRect(draw_pos + ImVec2(0, line_visible_n0 * g.FontSize), draw_pos + ImVec2(frame_size.x, line_visible_n1 * g.FontSize), IM_COL32(255, 0, 0, 255));
5455
5456
// Calculate blinking cursor position
5457
const ImVec2 cursor_offset = render_cursor && state ? InputTextLineIndexGetPosOffset(g, state, line_index, buf_display, buf_display_end, state->Stb->cursor) : ImVec2(0.0f, 0.0f);
5458
ImVec2 draw_scroll;
5459
5460
// Render text. We currently only render selection when the widget is active or while scrolling.
5461
const ImU32 text_col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5462
if (render_cursor || render_selection)
5463
{
5464
// Render text (with cursor and selection)
5465
// This is going to be messy. We need to:
5466
// - Display the text (this alone can be more easily clipped)
5467
// - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5468
// - Measure text height (for scrollbar)
5469
// 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)
5470
// FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5471
IM_ASSERT(state != NULL);
5472
state->LineCount = line_count;
5473
5474
// Scroll
5475
float new_scroll_y = scroll_y;
5476
if (render_cursor && state->CursorFollow)
5477
{
5478
// Horizontal scroll in chunks of quarter width
5479
if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5480
{
5481
const float scroll_increment_x = inner_size.x * 0.25f;
5482
const float visible_width = inner_size.x - style.FramePadding.x;
5483
if (cursor_offset.x < state->Scroll.x)
5484
state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5485
else if (cursor_offset.x - visible_width >= state->Scroll.x)
5486
state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5487
}
5488
else
5489
{
5490
state->Scroll.x = 0.0f;
5491
}
5492
5493
// Vertical scroll
5494
if (is_multiline)
5495
{
5496
// Test if cursor is vertically visible
5497
if (cursor_offset.y - g.FontSize < scroll_y)
5498
new_scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5499
else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5500
new_scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5501
}
5502
state->CursorFollow = false;
5503
}
5504
if (state->CursorCenterY)
5505
{
5506
if (is_multiline)
5507
new_scroll_y = cursor_offset.y - g.FontSize - (inner_size.y * 0.5f - style.FramePadding.y);
5508
state->CursorCenterY = false;
5509
render_cursor = false;
5510
}
5511
if (new_scroll_y != scroll_y)
5512
{
5513
const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5514
scroll_y = ImClamp(new_scroll_y, 0.0f, scroll_max_y);
5515
draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5516
draw_window->Scroll.y = scroll_y;
5517
CalcClipRectVisibleItemsY(clip_rect, draw_pos, g.FontSize, &line_visible_n0, &line_visible_n1);
5518
line_visible_n1 = ImMin(line_visible_n1, line_count);
5519
}
5520
5521
// Draw selection
5522
draw_scroll.x = state->Scroll.x;
5523
if (render_selection)
5524
{
5525
const ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests.
5526
const 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.
5527
const float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5528
const float bg_eol_width = IM_TRUNC(g.FontBaked->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5529
5530
const char* text_selected_begin = buf_display + ImMin(state->Stb->select_start, state->Stb->select_end);
5531
const char* text_selected_end = buf_display + ImMax(state->Stb->select_start, state->Stb->select_end);
5532
for (int line_n = line_visible_n0; line_n < line_visible_n1; line_n++)
5533
{
5534
const char* p = line_index->get_line_begin(buf_display, line_n);
5535
const char* p_eol = line_index->get_line_end(buf_display, line_n);
5536
const bool p_eol_is_wrap = (p_eol < buf_display_end && *p_eol != '\n');
5537
if (p_eol_is_wrap)
5538
p_eol++;
5539
const char* line_selected_begin = (text_selected_begin > p) ? text_selected_begin : p;
5540
const char* line_selected_end = (text_selected_end < p_eol) ? text_selected_end : p_eol;
5541
5542
float rect_width = 0.0f;
5543
if (line_selected_begin < line_selected_end)
5544
rect_width += CalcTextSize(line_selected_begin, line_selected_end).x;
5545
if (text_selected_begin <= p_eol && text_selected_end > p_eol && !p_eol_is_wrap)
5546
rect_width += bg_eol_width; // So we can see selected empty lines
5547
if (rect_width == 0.0f)
5548
continue;
5549
5550
ImRect rect;
5551
rect.Min.x = draw_pos.x - draw_scroll.x + CalcTextSize(p, line_selected_begin).x;
5552
rect.Min.y = draw_pos.y - draw_scroll.y + line_n * g.FontSize;
5553
rect.Max.x = rect.Min.x + rect_width;
5554
rect.Max.y = rect.Min.y + bg_offy_dn + g.FontSize;
5555
rect.Min.y -= bg_offy_up;
5556
rect.ClipWith(clip_rect);
5557
draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5558
}
5559
}
5560
}
5561
5562
// Find render position for right alignment (single-line only)
5563
if (g.ActiveId != id && flags & ImGuiInputTextFlags_ElideLeft)
5564
draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5565
//draw_scroll.x = state->Scroll.x; // Preserve scroll when inactive?
5566
5567
// Render text
5568
if ((is_multiline || (buf_display_end - buf_display) < buf_display_max_length) && (text_col & IM_COL32_A_MASK) && (line_visible_n0 < line_visible_n1))
5569
g.Font->RenderText(draw_window->DrawList, g.FontSize, g.FontWeight,
5570
draw_pos - draw_scroll + ImVec2(0.0f, line_visible_n0 * g.FontSize),
5571
text_col, clip_rect.AsVec4(),
5572
line_index->get_line_begin(buf_display, line_visible_n0),
5573
line_index->get_line_end(buf_display, line_visible_n1 - 1),
5574
wrap_width, ImDrawTextFlags_WrapKeepBlanks | ImDrawTextFlags_CpuFineClip);
5575
5576
// Render blinking cursor
5577
if (render_cursor)
5578
{
5579
state->CursorAnim += io.DeltaTime;
5580
bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5581
ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5582
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);
5583
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5584
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_InputTextCursor), 1.0f); // FIXME-DPI: Cursor thickness (#7031)
5585
5586
// 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.)
5587
// This is required for some backends (SDL3) to start emitting character/text inputs.
5588
// As per #6341, make sure we don't set that on the deactivating frame.
5589
if (!is_readonly && g.ActiveId == id)
5590
{
5591
ImGuiPlatformImeData* ime_data = &g.PlatformImeData; // (this is a public struct, passed to io.Platform_SetImeDataFn() handler)
5592
ime_data->WantVisible = true;
5593
ime_data->WantTextInput = true;
5594
ime_data->InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5595
ime_data->InputLineHeight = g.FontSize;
5596
ime_data->ViewportId = window->Viewport->ID;
5597
}
5598
}
5599
5600
if (is_password && !is_displaying_hint)
5601
PopPasswordFont();
5602
5603
if (is_multiline)
5604
{
5605
// For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5606
Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5607
g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5608
EndChild();
5609
item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5610
5611
// ...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...
5612
// FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5613
EndGroup();
5614
if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5615
{
5616
g.LastItemData.ID = id;
5617
g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5618
g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5619
}
5620
}
5621
if (state)
5622
state->TextSrc = NULL;
5623
5624
// Log as text
5625
if (g.LogEnabled && (!is_password || is_displaying_hint))
5626
{
5627
LogSetNextTextDecoration("{", "}");
5628
LogRenderedText(&draw_pos, buf_display, buf_display_end);
5629
}
5630
5631
if (label_size.x > 0)
5632
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
5633
5634
if (value_changed)
5635
MarkItemEdited(id);
5636
5637
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5638
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5639
return validated;
5640
else
5641
return value_changed;
5642
}
5643
5644
void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5645
{
5646
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5647
ImGuiContext& g = *GImGui;
5648
ImStb::STB_TexteditState* stb_state = state->Stb;
5649
ImStb::StbUndoState* undo_state = &stb_state->undostate;
5650
Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5651
DebugLocateItemOnHover(state->ID);
5652
Text("TextLen: %d, Cursor: %d%s, Selection: %d..%d", state->TextLen, stb_state->cursor,
5653
(state->Flags & ImGuiInputTextFlags_WordWrap) ? (state->LastMoveDirectionLR == ImGuiDir_Left ? " (L)" : " (R)") : "",
5654
stb_state->select_start, stb_state->select_end);
5655
Text("BufCapacity: %d, LineCount: %d", state->BufCapacity, state->LineCount);
5656
Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5657
Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5658
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);
5659
if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5660
{
5661
PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5662
for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5663
{
5664
ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5665
const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5666
if (undo_rec_type == ' ')
5667
BeginDisabled();
5668
const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5669
const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5670
Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5671
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);
5672
if (undo_rec_type == ' ')
5673
EndDisabled();
5674
}
5675
PopStyleVar();
5676
}
5677
EndChild();
5678
#else
5679
IM_UNUSED(state);
5680
#endif
5681
}
5682
5683
//-------------------------------------------------------------------------
5684
// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5685
//-------------------------------------------------------------------------
5686
// - ColorEdit3()
5687
// - ColorEdit4()
5688
// - ColorPicker3()
5689
// - RenderColorRectWithAlphaCheckerboard() [Internal]
5690
// - ColorPicker4()
5691
// - ColorButton()
5692
// - SetColorEditOptions()
5693
// - ColorTooltip() [Internal]
5694
// - ColorEditOptionsPopup() [Internal]
5695
// - ColorPickerOptionsPopup() [Internal]
5696
//-------------------------------------------------------------------------
5697
5698
bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5699
{
5700
return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5701
}
5702
5703
static void ColorEditRestoreH(const float* col, float* H)
5704
{
5705
ImGuiContext& g = *GImGui;
5706
IM_ASSERT(g.ColorEditCurrentID != 0);
5707
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5708
return;
5709
*H = g.ColorEditSavedHue;
5710
}
5711
5712
// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5713
// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5714
static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5715
{
5716
ImGuiContext& g = *GImGui;
5717
IM_ASSERT(g.ColorEditCurrentID != 0);
5718
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5719
return;
5720
5721
// When S == 0, H is undefined.
5722
// When H == 1 it wraps around to 0.
5723
if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5724
*H = g.ColorEditSavedHue;
5725
5726
// When V == 0, S is undefined.
5727
if (*V == 0.0f)
5728
*S = g.ColorEditSavedSat;
5729
}
5730
5731
// Edit colors components (each component in 0.0f..1.0f range).
5732
// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5733
// 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.
5734
bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5735
{
5736
ImGuiWindow* window = GetCurrentWindow();
5737
if (window->SkipItems)
5738
return false;
5739
5740
ImGuiContext& g = *GImGui;
5741
const ImGuiStyle& style = g.Style;
5742
const float square_sz = GetFrameHeight();
5743
const char* label_display_end = FindRenderedTextEnd(label);
5744
float w_full = CalcItemWidth();
5745
g.NextItemData.ClearFlags();
5746
5747
BeginGroup();
5748
PushID(label);
5749
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5750
if (set_current_color_edit_id)
5751
g.ColorEditCurrentID = window->IDStack.back();
5752
5753
// If we're not showing any slider there's no point in doing any HSV conversions
5754
const ImGuiColorEditFlags flags_untouched = flags;
5755
if (flags & ImGuiColorEditFlags_NoInputs)
5756
flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5757
5758
// Context menu: display and modify options (before defaults are applied)
5759
if (!(flags & ImGuiColorEditFlags_NoOptions))
5760
ColorEditOptionsPopup(col, flags);
5761
5762
// Read stored options
5763
if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5764
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5765
if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5766
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5767
if (!(flags & ImGuiColorEditFlags_PickerMask_))
5768
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5769
if (!(flags & ImGuiColorEditFlags_InputMask_))
5770
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5771
flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5772
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5773
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5774
5775
const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5776
const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5777
const int components = alpha ? 4 : 3;
5778
const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5779
const float w_inputs = ImMax(w_full - w_button, 1.0f);
5780
w_full = w_inputs + w_button;
5781
5782
// Convert to the formats we need
5783
float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5784
if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5785
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5786
else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5787
{
5788
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5789
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5790
ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5791
}
5792
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]) };
5793
5794
bool value_changed = false;
5795
bool value_changed_as_float = false;
5796
5797
const ImVec2 pos = window->DC.CursorPos;
5798
const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5799
window->DC.CursorPos.x = pos.x + inputs_offset_x;
5800
5801
if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5802
{
5803
// RGB/HSV 0..255 Sliders
5804
const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5805
const float w_per_component = IM_TRUNC(w_items / components);
5806
const bool draw_color_marker = (flags & (ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_NoColorMarkers)) == 0;
5807
5808
const bool hide_prefix = draw_color_marker || (w_per_component <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5809
static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5810
static const char* fmt_table_int[3][4] =
5811
{
5812
{ "%3d", "%3d", "%3d", "%3d" }, // Short display
5813
{ "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5814
{ "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5815
};
5816
static const char* fmt_table_float[3][4] =
5817
{
5818
{ "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5819
{ "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5820
{ "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5821
};
5822
const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5823
const ImGuiSliderFlags drag_flags = draw_color_marker ? ImGuiSliderFlags_ColorMarkers : ImGuiSliderFlags_None;
5824
5825
float prev_split = 0.0f;
5826
for (int n = 0; n < components; n++)
5827
{
5828
if (n > 0)
5829
SameLine(0, style.ItemInnerSpacing.x);
5830
float next_split = IM_TRUNC(w_items * (n + 1) / components);
5831
SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5832
prev_split = next_split;
5833
if (draw_color_marker)
5834
SetNextItemColorMarker(GDefaultRgbaColorMarkers[n]);
5835
5836
// FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5837
if (flags & ImGuiColorEditFlags_Float)
5838
{
5839
value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n], drag_flags);
5840
value_changed_as_float |= value_changed;
5841
}
5842
else
5843
{
5844
value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n], drag_flags);
5845
}
5846
if (!(flags & ImGuiColorEditFlags_NoOptions))
5847
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5848
}
5849
}
5850
else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5851
{
5852
// RGB Hexadecimal Input
5853
char buf[64];
5854
if (alpha)
5855
ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255));
5856
else
5857
ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5858
SetNextItemWidth(w_inputs);
5859
if (InputText("##Text", buf, IM_COUNTOF(buf), ImGuiInputTextFlags_CharsUppercase))
5860
{
5861
value_changed = true;
5862
char* p = buf;
5863
while (*p == '#' || ImCharIsBlankA(*p))
5864
p++;
5865
i[0] = i[1] = i[2] = 0;
5866
i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5867
int r;
5868
if (alpha)
5869
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)
5870
else
5871
r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5872
IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5873
}
5874
if (!(flags & ImGuiColorEditFlags_NoOptions))
5875
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5876
}
5877
5878
ImGuiWindow* picker_active_window = NULL;
5879
if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5880
{
5881
const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5882
window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5883
5884
const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5885
if (ColorButton("##ColorButton", col_v4, flags))
5886
{
5887
if (!(flags & ImGuiColorEditFlags_NoPicker))
5888
{
5889
// Store current color and open a picker
5890
g.ColorPickerRef = col_v4;
5891
OpenPopup("picker");
5892
SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5893
}
5894
}
5895
if (!(flags & ImGuiColorEditFlags_NoOptions))
5896
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5897
5898
if (BeginPopup("picker"))
5899
{
5900
if (g.CurrentWindow->BeginCount == 1)
5901
{
5902
picker_active_window = g.CurrentWindow;
5903
if (label != label_display_end)
5904
{
5905
TextEx(label, label_display_end);
5906
Spacing();
5907
}
5908
ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5909
ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5910
SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5911
value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
5912
}
5913
EndPopup();
5914
}
5915
}
5916
5917
if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5918
{
5919
// Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5920
// but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5921
SameLine(0.0f, style.ItemInnerSpacing.x);
5922
window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5923
TextEx(label, label_display_end);
5924
}
5925
5926
// Convert back
5927
if (value_changed && picker_active_window == NULL)
5928
{
5929
if (!value_changed_as_float)
5930
for (int n = 0; n < 4; n++)
5931
f[n] = i[n] / 255.0f;
5932
if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5933
{
5934
g.ColorEditSavedHue = f[0];
5935
g.ColorEditSavedSat = f[1];
5936
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5937
g.ColorEditSavedID = g.ColorEditCurrentID;
5938
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
5939
}
5940
if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5941
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5942
5943
col[0] = f[0];
5944
col[1] = f[1];
5945
col[2] = f[2];
5946
if (alpha)
5947
col[3] = f[3];
5948
}
5949
5950
if (set_current_color_edit_id)
5951
g.ColorEditCurrentID = 0;
5952
PopID();
5953
EndGroup();
5954
5955
// Drag and Drop Target
5956
// NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5957
if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5958
{
5959
bool accepted_drag_drop = false;
5960
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5961
{
5962
memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5963
value_changed = accepted_drag_drop = true;
5964
}
5965
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5966
{
5967
memcpy((float*)col, payload->Data, sizeof(float) * components);
5968
value_changed = accepted_drag_drop = true;
5969
}
5970
5971
// Drag-drop payloads are always RGB
5972
if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5973
ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
5974
EndDragDropTarget();
5975
}
5976
5977
// When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5978
if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5979
g.LastItemData.ID = g.ActiveId;
5980
5981
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5982
MarkItemEdited(g.LastItemData.ID);
5983
5984
return value_changed;
5985
}
5986
5987
bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5988
{
5989
float col4[4] = { col[0], col[1], col[2], 1.0f };
5990
if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
5991
return false;
5992
col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5993
return true;
5994
}
5995
5996
// Helper for ColorPicker4()
5997
static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5998
{
5999
ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
6000
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));
6001
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
6002
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));
6003
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
6004
}
6005
6006
// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6007
// (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.)
6008
// 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..)
6009
// 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)
6010
bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
6011
{
6012
ImGuiContext& g = *GImGui;
6013
ImGuiWindow* window = GetCurrentWindow();
6014
if (window->SkipItems)
6015
return false;
6016
6017
ImDrawList* draw_list = window->DrawList;
6018
ImGuiStyle& style = g.Style;
6019
ImGuiIO& io = g.IO;
6020
6021
const float width = CalcItemWidth();
6022
const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
6023
g.NextItemData.ClearFlags();
6024
6025
PushID(label);
6026
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
6027
if (set_current_color_edit_id)
6028
g.ColorEditCurrentID = window->IDStack.back();
6029
BeginGroup();
6030
6031
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6032
flags |= ImGuiColorEditFlags_NoSmallPreview;
6033
6034
// Context menu: display and store options.
6035
if (!(flags & ImGuiColorEditFlags_NoOptions))
6036
ColorPickerOptionsPopup(col, flags);
6037
6038
// Read stored options
6039
if (!(flags & ImGuiColorEditFlags_PickerMask_))
6040
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
6041
if (!(flags & ImGuiColorEditFlags_InputMask_))
6042
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
6043
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
6044
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
6045
if (!(flags & ImGuiColorEditFlags_NoOptions))
6046
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
6047
6048
// Setup
6049
int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
6050
bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
6051
ImVec2 picker_pos = window->DC.CursorPos;
6052
float square_sz = GetFrameHeight();
6053
float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
6054
float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
6055
float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
6056
float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
6057
float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
6058
6059
float backup_initial_col[4];
6060
memcpy(backup_initial_col, col, components * sizeof(float));
6061
6062
float wheel_thickness = sv_picker_size * 0.08f;
6063
float wheel_r_outer = sv_picker_size * 0.50f;
6064
float wheel_r_inner = wheel_r_outer - wheel_thickness;
6065
ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
6066
6067
// Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
6068
float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
6069
ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
6070
ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
6071
ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
6072
6073
float H = col[0], S = col[1], V = col[2];
6074
float R = col[0], G = col[1], B = col[2];
6075
if (flags & ImGuiColorEditFlags_InputRGB)
6076
{
6077
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
6078
ColorConvertRGBtoHSV(R, G, B, H, S, V);
6079
ColorEditRestoreHS(col, &H, &S, &V);
6080
}
6081
else if (flags & ImGuiColorEditFlags_InputHSV)
6082
{
6083
ColorConvertHSVtoRGB(H, S, V, R, G, B);
6084
}
6085
6086
bool value_changed = false, value_changed_h = false, value_changed_sv = false;
6087
6088
PushItemFlag(ImGuiItemFlags_NoNav, true);
6089
if (flags & ImGuiColorEditFlags_PickerHueWheel)
6090
{
6091
// Hue wheel + SV triangle logic
6092
InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
6093
if (IsItemActive() && !is_readonly)
6094
{
6095
ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
6096
ImVec2 current_off = g.IO.MousePos - wheel_center;
6097
float initial_dist2 = ImLengthSqr(initial_off);
6098
if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
6099
{
6100
// Interactive with Hue wheel
6101
H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
6102
if (H < 0.0f)
6103
H += 1.0f;
6104
value_changed = value_changed_h = true;
6105
}
6106
float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
6107
float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
6108
if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
6109
{
6110
// Interacting with SV triangle
6111
ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
6112
if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
6113
current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
6114
float uu, vv, ww;
6115
ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
6116
V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
6117
S = ImClamp(uu / V, 0.0001f, 1.0f);
6118
value_changed = value_changed_sv = true;
6119
}
6120
}
6121
if (!(flags & ImGuiColorEditFlags_NoOptions))
6122
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6123
}
6124
else if (flags & ImGuiColorEditFlags_PickerHueBar)
6125
{
6126
// SV rectangle logic
6127
InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
6128
if (IsItemActive() && !is_readonly)
6129
{
6130
S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
6131
V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6132
ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
6133
value_changed = value_changed_sv = true;
6134
}
6135
if (!(flags & ImGuiColorEditFlags_NoOptions))
6136
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
6137
6138
// Hue bar logic
6139
SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
6140
InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
6141
if (IsItemActive() && !is_readonly)
6142
{
6143
H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6144
value_changed = value_changed_h = true;
6145
}
6146
}
6147
6148
// Alpha bar logic
6149
if (alpha_bar)
6150
{
6151
SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
6152
InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
6153
if (IsItemActive())
6154
{
6155
col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
6156
value_changed = true;
6157
}
6158
}
6159
PopItemFlag(); // ImGuiItemFlags_NoNav
6160
6161
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6162
{
6163
SameLine(0, style.ItemInnerSpacing.x);
6164
BeginGroup();
6165
}
6166
6167
if (!(flags & ImGuiColorEditFlags_NoLabel))
6168
{
6169
const char* label_display_end = FindRenderedTextEnd(label);
6170
if (label != label_display_end)
6171
{
6172
if ((flags & ImGuiColorEditFlags_NoSidePreview))
6173
SameLine(0, style.ItemInnerSpacing.x);
6174
TextEx(label, label_display_end);
6175
}
6176
}
6177
6178
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
6179
{
6180
PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
6181
ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6182
if ((flags & ImGuiColorEditFlags_NoLabel))
6183
Text("Current");
6184
6185
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip;
6186
ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
6187
if (ref_col != NULL)
6188
{
6189
Text("Original");
6190
ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
6191
if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
6192
{
6193
memcpy(col, ref_col, components * sizeof(float));
6194
value_changed = true;
6195
}
6196
}
6197
PopItemFlag();
6198
EndGroup();
6199
}
6200
6201
// Convert back color to RGB
6202
if (value_changed_h || value_changed_sv)
6203
{
6204
if (flags & ImGuiColorEditFlags_InputRGB)
6205
{
6206
ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
6207
g.ColorEditSavedHue = H;
6208
g.ColorEditSavedSat = S;
6209
g.ColorEditSavedID = g.ColorEditCurrentID;
6210
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
6211
}
6212
else if (flags & ImGuiColorEditFlags_InputHSV)
6213
{
6214
col[0] = H;
6215
col[1] = S;
6216
col[2] = V;
6217
}
6218
}
6219
6220
// R,G,B and H,S,V slider color editor
6221
bool value_changed_fix_hue_wrap = false;
6222
if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
6223
{
6224
PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
6225
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview;
6226
ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
6227
if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6228
if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
6229
{
6230
// FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
6231
// 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)
6232
value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
6233
value_changed = true;
6234
}
6235
if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6236
value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
6237
if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6238
value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
6239
PopItemWidth();
6240
}
6241
6242
// Try to cancel hue wrap (after ColorEdit4 call), if any
6243
if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
6244
{
6245
float new_H, new_S, new_V;
6246
ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
6247
if (new_H <= 0 && H > 0)
6248
{
6249
if (new_V <= 0 && V != new_V)
6250
ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
6251
else if (new_S <= 0)
6252
ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
6253
}
6254
}
6255
6256
if (value_changed)
6257
{
6258
if (flags & ImGuiColorEditFlags_InputRGB)
6259
{
6260
R = col[0];
6261
G = col[1];
6262
B = col[2];
6263
ColorConvertRGBtoHSV(R, G, B, H, S, V);
6264
ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
6265
}
6266
else if (flags & ImGuiColorEditFlags_InputHSV)
6267
{
6268
H = col[0];
6269
S = col[1];
6270
V = col[2];
6271
ColorConvertHSVtoRGB(H, S, V, R, G, B);
6272
}
6273
}
6274
6275
const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
6276
const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
6277
const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
6278
const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
6279
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) };
6280
6281
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);
6282
ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
6283
ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
6284
6285
ImVec2 sv_cursor_pos;
6286
6287
if (flags & ImGuiColorEditFlags_PickerHueWheel)
6288
{
6289
// Render Hue Wheel
6290
const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
6291
const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
6292
for (int n = 0; n < 6; n++)
6293
{
6294
const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
6295
const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
6296
const int vert_start_idx = draw_list->VtxBuffer.Size;
6297
draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6298
draw_list->PathStroke(col_white, 0, wheel_thickness);
6299
const int vert_end_idx = draw_list->VtxBuffer.Size;
6300
6301
// Paint colors over existing vertices
6302
ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6303
ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6304
ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6305
}
6306
6307
// Render Cursor + preview on Hue Wheel
6308
float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6309
float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6310
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);
6311
float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6312
int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6313
draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6314
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6315
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6316
6317
// Render SV triangle (rotated according to hue)
6318
ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6319
ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6320
ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6321
ImVec2 uv_white = GetFontTexUvWhitePixel();
6322
draw_list->PrimReserve(3, 3);
6323
draw_list->PrimVtx(tra, uv_white, hue_color32);
6324
draw_list->PrimVtx(trb, uv_white, col_black);
6325
draw_list->PrimVtx(trc, uv_white, col_white);
6326
draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6327
sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6328
}
6329
else if (flags & ImGuiColorEditFlags_PickerHueBar)
6330
{
6331
// Render SV Square
6332
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6333
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6334
RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6335
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
6336
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);
6337
6338
// Render Hue Bar
6339
for (int i = 0; i < 6; ++i)
6340
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]);
6341
float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6342
RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6343
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);
6344
}
6345
6346
// Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6347
float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6348
int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6349
draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6350
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6351
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6352
6353
// Render alpha bar
6354
if (alpha_bar)
6355
{
6356
float alpha = ImSaturate(col[3]);
6357
ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6358
RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6359
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);
6360
float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6361
RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6362
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);
6363
}
6364
6365
EndGroup();
6366
6367
if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6368
value_changed = false;
6369
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6370
MarkItemEdited(g.LastItemData.ID);
6371
6372
if (set_current_color_edit_id)
6373
g.ColorEditCurrentID = 0;
6374
PopID();
6375
6376
return value_changed;
6377
}
6378
6379
// A little color square. Return true when clicked.
6380
// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6381
// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6382
// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6383
bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6384
{
6385
ImGuiWindow* window = GetCurrentWindow();
6386
if (window->SkipItems)
6387
return false;
6388
6389
ImGuiContext& g = *GImGui;
6390
const ImGuiID id = window->GetID(desc_id);
6391
const float default_size = GetFrameHeight();
6392
const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6393
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6394
ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6395
if (!ItemAdd(bb, id))
6396
return false;
6397
6398
bool hovered, held;
6399
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6400
6401
if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque))
6402
flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf);
6403
6404
ImVec4 col_rgb = col;
6405
if (flags & ImGuiColorEditFlags_InputHSV)
6406
ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6407
6408
ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6409
float grid_step = ImMin(size.x, size.y) / 2.99f;
6410
float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6411
ImRect bb_inner = bb;
6412
float off = 0.0f;
6413
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6414
{
6415
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.
6416
bb_inner.Expand(off);
6417
}
6418
if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6419
{
6420
float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6421
if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6422
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);
6423
else
6424
window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight);
6425
window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6426
}
6427
else
6428
{
6429
// Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6430
ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb;
6431
if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0)
6432
RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6433
else
6434
window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6435
}
6436
RenderNavCursor(bb, id);
6437
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6438
{
6439
if (g.Style.FrameBorderSize > 0.0f)
6440
RenderFrameBorder(bb.Min, bb.Max, rounding);
6441
else
6442
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border // FIXME-DPI
6443
}
6444
6445
// Drag and Drop Source
6446
// NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6447
if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6448
{
6449
if (flags & ImGuiColorEditFlags_NoAlpha)
6450
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6451
else
6452
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6453
ColorButton(desc_id, col, flags);
6454
SameLine();
6455
TextEx("Color");
6456
EndDragDropSource();
6457
}
6458
6459
// Tooltip
6460
if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6461
ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_));
6462
6463
return pressed;
6464
}
6465
6466
// Initialize/override default color options
6467
void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6468
{
6469
ImGuiContext& g = *GImGui;
6470
if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6471
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6472
if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6473
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6474
if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6475
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6476
if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6477
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6478
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6479
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6480
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6481
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6482
g.ColorEditOptions = flags;
6483
}
6484
6485
// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6486
void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6487
{
6488
ImGuiContext& g = *GImGui;
6489
6490
if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6491
return;
6492
const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6493
if (text_end > text)
6494
{
6495
TextEx(text, text_end);
6496
Separator();
6497
}
6498
6499
ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6500
ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6501
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]);
6502
ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_;
6503
ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz);
6504
SameLine();
6505
if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6506
{
6507
if (flags & ImGuiColorEditFlags_NoAlpha)
6508
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]);
6509
else
6510
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]);
6511
}
6512
else if (flags & ImGuiColorEditFlags_InputHSV)
6513
{
6514
if (flags & ImGuiColorEditFlags_NoAlpha)
6515
Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6516
else
6517
Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6518
}
6519
EndTooltip();
6520
}
6521
6522
void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6523
{
6524
bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6525
bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6526
if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6527
return;
6528
6529
ImGuiContext& g = *GImGui;
6530
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6531
ImGuiColorEditFlags opts = g.ColorEditOptions;
6532
if (allow_opt_inputs)
6533
{
6534
if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6535
if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6536
if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6537
}
6538
if (allow_opt_datatype)
6539
{
6540
if (allow_opt_inputs) Separator();
6541
if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6542
if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6543
}
6544
6545
if (allow_opt_inputs || allow_opt_datatype)
6546
Separator();
6547
if (Button("Copy as..", ImVec2(-1, 0)))
6548
OpenPopup("Copy");
6549
if (BeginPopup("Copy"))
6550
{
6551
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]);
6552
char buf[64];
6553
ImFormatString(buf, IM_COUNTOF(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6554
if (Selectable(buf))
6555
SetClipboardText(buf);
6556
ImFormatString(buf, IM_COUNTOF(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6557
if (Selectable(buf))
6558
SetClipboardText(buf);
6559
ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X", cr, cg, cb);
6560
if (Selectable(buf))
6561
SetClipboardText(buf);
6562
if (!(flags & ImGuiColorEditFlags_NoAlpha))
6563
{
6564
ImFormatString(buf, IM_COUNTOF(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6565
if (Selectable(buf))
6566
SetClipboardText(buf);
6567
}
6568
EndPopup();
6569
}
6570
6571
g.ColorEditOptions = opts;
6572
PopItemFlag();
6573
EndPopup();
6574
}
6575
6576
void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6577
{
6578
bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6579
bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6580
if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6581
return;
6582
6583
ImGuiContext& g = *GImGui;
6584
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6585
if (allow_opt_picker)
6586
{
6587
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
6588
PushItemWidth(picker_size.x);
6589
for (int picker_type = 0; picker_type < 2; picker_type++)
6590
{
6591
// Draw small/thumbnail version of each picker type (over an invisible button for selection)
6592
if (picker_type > 0) Separator();
6593
PushID(picker_type);
6594
ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6595
if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6596
if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6597
ImVec2 backup_pos = GetCursorScreenPos();
6598
if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6599
g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6600
SetCursorScreenPos(backup_pos);
6601
ImVec4 previewing_ref_col;
6602
memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6603
ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6604
PopID();
6605
}
6606
PopItemWidth();
6607
}
6608
if (allow_opt_alpha_bar)
6609
{
6610
if (allow_opt_picker) Separator();
6611
CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
6612
}
6613
PopItemFlag();
6614
EndPopup();
6615
}
6616
6617
//-------------------------------------------------------------------------
6618
// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6619
//-------------------------------------------------------------------------
6620
// - TreeNode()
6621
// - TreeNodeV()
6622
// - TreeNodeEx()
6623
// - TreeNodeExV()
6624
// - TreeNodeStoreStackData() [Internal]
6625
// - TreeNodeBehavior() [Internal]
6626
// - TreePush()
6627
// - TreePop()
6628
// - GetTreeNodeToLabelSpacing()
6629
// - SetNextItemOpen()
6630
// - CollapsingHeader()
6631
//-------------------------------------------------------------------------
6632
6633
bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6634
{
6635
va_list args;
6636
va_start(args, fmt);
6637
bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6638
va_end(args);
6639
return is_open;
6640
}
6641
6642
bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6643
{
6644
va_list args;
6645
va_start(args, fmt);
6646
bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6647
va_end(args);
6648
return is_open;
6649
}
6650
6651
bool ImGui::TreeNode(const char* label)
6652
{
6653
ImGuiWindow* window = GetCurrentWindow();
6654
if (window->SkipItems)
6655
return false;
6656
ImGuiID id = window->GetID(label);
6657
return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6658
}
6659
6660
bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6661
{
6662
return TreeNodeExV(str_id, 0, fmt, args);
6663
}
6664
6665
bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6666
{
6667
return TreeNodeExV(ptr_id, 0, fmt, args);
6668
}
6669
6670
bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6671
{
6672
ImGuiWindow* window = GetCurrentWindow();
6673
if (window->SkipItems)
6674
return false;
6675
ImGuiID id = window->GetID(label);
6676
return TreeNodeBehavior(id, flags, label, NULL);
6677
}
6678
6679
bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6680
{
6681
va_list args;
6682
va_start(args, fmt);
6683
bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6684
va_end(args);
6685
return is_open;
6686
}
6687
6688
bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6689
{
6690
va_list args;
6691
va_start(args, fmt);
6692
bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6693
va_end(args);
6694
return is_open;
6695
}
6696
6697
bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6698
{
6699
ImGuiWindow* window = GetCurrentWindow();
6700
if (window->SkipItems)
6701
return false;
6702
6703
ImGuiID id = window->GetID(str_id);
6704
const char* label, *label_end;
6705
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6706
return TreeNodeBehavior(id, flags, label, label_end);
6707
}
6708
6709
bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6710
{
6711
ImGuiWindow* window = GetCurrentWindow();
6712
if (window->SkipItems)
6713
return false;
6714
6715
ImGuiID id = window->GetID(ptr_id);
6716
const char* label, *label_end;
6717
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6718
return TreeNodeBehavior(id, flags, label, label_end);
6719
}
6720
6721
bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6722
{
6723
ImGuiContext& g = *GImGui;
6724
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6725
return storage->GetInt(storage_id, 0) != 0;
6726
}
6727
6728
void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6729
{
6730
ImGuiContext& g = *GImGui;
6731
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6732
storage->SetInt(storage_id, open ? 1 : 0);
6733
}
6734
6735
bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6736
{
6737
if (flags & ImGuiTreeNodeFlags_Leaf)
6738
return true;
6739
6740
// We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6741
ImGuiContext& g = *GImGui;
6742
ImGuiWindow* window = g.CurrentWindow;
6743
ImGuiStorage* storage = window->DC.StateStorage;
6744
6745
bool is_open;
6746
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6747
{
6748
if (g.NextItemData.OpenCond & ImGuiCond_Always)
6749
{
6750
is_open = g.NextItemData.OpenVal;
6751
TreeNodeSetOpen(storage_id, is_open);
6752
}
6753
else
6754
{
6755
// We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6756
const int stored_value = storage->GetInt(storage_id, -1);
6757
if (stored_value == -1)
6758
{
6759
is_open = g.NextItemData.OpenVal;
6760
TreeNodeSetOpen(storage_id, is_open);
6761
}
6762
else
6763
{
6764
is_open = stored_value != 0;
6765
}
6766
}
6767
}
6768
else
6769
{
6770
is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6771
}
6772
6773
// When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6774
// NB- If we are above max depth we still allow manually opened nodes to be logged.
6775
if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6776
is_open = true;
6777
6778
return is_open;
6779
}
6780
6781
// Store ImGuiTreeNodeStackData for just submitted node.
6782
// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6783
static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags, float x1)
6784
{
6785
ImGuiContext& g = *GImGui;
6786
ImGuiWindow* window = g.CurrentWindow;
6787
6788
g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6789
ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6790
tree_node_data->ID = g.LastItemData.ID;
6791
tree_node_data->TreeFlags = flags;
6792
tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6793
tree_node_data->NavRect = g.LastItemData.NavRect;
6794
6795
// Initially I tried to latch value for GetColorU32(ImGuiCol_TreeLines) but it's not a good trade-off for very large trees.
6796
const bool draw_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) != 0;
6797
tree_node_data->DrawLinesX1 = draw_lines ? (x1 + g.FontSize * 0.5f + g.Style.FramePadding.x) : +FLT_MAX;
6798
tree_node_data->DrawLinesTableColumn = (draw_lines && g.CurrentTable) ? (ImGuiTableColumnIdx)g.CurrentTable->CurrentColumn : -1;
6799
tree_node_data->DrawLinesToNodesY2 = -FLT_MAX;
6800
window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6801
if (flags & ImGuiTreeNodeFlags_DrawLinesToNodes)
6802
window->DC.TreeRecordsClippedNodesY2Mask |= (1 << window->DC.TreeDepth);
6803
}
6804
6805
bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6806
{
6807
ImGuiWindow* window = GetCurrentWindow();
6808
if (window->SkipItems)
6809
return false;
6810
6811
ImGuiContext& g = *GImGui;
6812
const ImGuiStyle& style = g.Style;
6813
6814
// When not framed, we vertically increase height up to typical framed widget height
6815
const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6816
const bool use_frame_padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding));
6817
const ImVec2 padding = use_frame_padding ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6818
6819
if (!label_end)
6820
label_end = FindRenderedTextEnd(label);
6821
const ImVec2 label_size = CalcTextSize(label, label_end, false);
6822
6823
const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6824
const float text_offset_y = use_frame_padding ? ImMax(style.FramePadding.y, window->DC.CurrLineTextBaseOffset) : window->DC.CurrLineTextBaseOffset; // Latch before ItemSize changes it
6825
const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6826
6827
const float frame_height = label_size.y + padding.y * 2;
6828
const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6829
const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL);
6830
ImRect frame_bb;
6831
frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6832
frame_bb.Min.y = window->DC.CursorPos.y + (text_offset_y - padding.y);
6833
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;
6834
frame_bb.Max.y = window->DC.CursorPos.y + (text_offset_y - padding.y) + frame_height;
6835
if (display_frame)
6836
{
6837
const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6838
frame_bb.Min.x -= outer_extend;
6839
frame_bb.Max.x += outer_extend;
6840
}
6841
6842
ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6843
ItemSize(ImVec2(text_width, frame_height), padding.y);
6844
6845
// For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6846
ImRect interact_bb = frame_bb;
6847
if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6848
interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6849
6850
// Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6851
ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6852
bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6853
6854
bool is_visible;
6855
if (span_all_columns || span_all_columns_label)
6856
{
6857
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6858
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6859
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6860
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6861
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6862
is_visible = ItemAdd(interact_bb, id);
6863
window->ClipRect.Min.x = backup_clip_rect_min_x;
6864
window->ClipRect.Max.x = backup_clip_rect_max_x;
6865
}
6866
else
6867
{
6868
is_visible = ItemAdd(interact_bb, id);
6869
}
6870
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6871
g.LastItemData.DisplayRect = frame_bb;
6872
6873
// If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsToParent enabled:
6874
// Store data for the current depth to allow returning to this node from any child item.
6875
// For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6876
// It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsToParent by default or move it to ImGuiStyle.
6877
bool store_tree_node_stack_data = false;
6878
if ((flags & ImGuiTreeNodeFlags_DrawLinesMask_) == 0)
6879
flags |= g.Style.TreeLinesFlags;
6880
const bool draw_tree_lines = (flags & (ImGuiTreeNodeFlags_DrawLinesFull | ImGuiTreeNodeFlags_DrawLinesToNodes)) && (frame_bb.Min.y < window->ClipRect.Max.y) && (g.Style.TreeLinesSize > 0.0f);
6881
if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6882
{
6883
store_tree_node_stack_data = draw_tree_lines;
6884
if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsToParent) && !g.NavIdIsAlive)
6885
if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6886
store_tree_node_stack_data = true;
6887
}
6888
6889
const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6890
if (!is_visible)
6891
{
6892
if ((flags & ImGuiTreeNodeFlags_DrawLinesToNodes) && (window->DC.TreeRecordsClippedNodesY2Mask & (1 << (window->DC.TreeDepth - 1))))
6893
{
6894
ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
6895
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.
6896
if (frame_bb.Min.y >= window->ClipRect.Max.y)
6897
window->DC.TreeRecordsClippedNodesY2Mask &= ~(1 << (window->DC.TreeDepth - 1)); // Done
6898
}
6899
if (is_open && store_tree_node_stack_data)
6900
TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
6901
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6902
TreePushOverrideID(id);
6903
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6904
return is_open;
6905
}
6906
6907
if (span_all_columns || span_all_columns_label)
6908
{
6909
TablePushBackgroundChannel();
6910
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6911
g.LastItemData.ClipRect = window->ClipRect;
6912
}
6913
6914
ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6915
if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6916
button_flags |= ImGuiButtonFlags_AllowOverlap;
6917
if (!is_leaf)
6918
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6919
6920
// We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6921
// allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6922
// When clicking on the rest of the tree node we always disallow keyboard modifiers.
6923
const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6924
const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6925
const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6926
6927
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6928
if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6929
flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6930
6931
// Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6932
// Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6933
// - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6934
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6935
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6936
// - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6937
// - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6938
// It is rather standard that arrow click react on Down rather than Up.
6939
// 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.
6940
if (is_mouse_x_over_arrow)
6941
button_flags |= ImGuiButtonFlags_PressedOnClick;
6942
else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6943
button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6944
else
6945
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6946
if (flags & ImGuiTreeNodeFlags_NoNavFocus)
6947
button_flags |= ImGuiButtonFlags_NoNavFocus;
6948
6949
bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6950
const bool was_selected = selected;
6951
6952
// Multi-selection support (header)
6953
if (is_multi_select)
6954
{
6955
// Handle multi-select + alter button flags for it
6956
MultiSelectItemHeader(id, &selected, &button_flags);
6957
if (is_mouse_x_over_arrow)
6958
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6959
}
6960
else
6961
{
6962
if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6963
button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6964
}
6965
6966
bool hovered, held;
6967
bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
6968
bool toggled = false;
6969
if (!is_leaf)
6970
{
6971
if (pressed && g.DragDropHoldJustPressedId != id)
6972
{
6973
if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6974
toggled = true; // Single click
6975
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6976
toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6977
if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6978
toggled = true; // Double click
6979
}
6980
else if (pressed && g.DragDropHoldJustPressedId == id)
6981
{
6982
IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6983
if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6984
toggled = true;
6985
else
6986
pressed = false; // Cancel press so it doesn't trigger selection.
6987
}
6988
6989
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6990
{
6991
toggled = true;
6992
NavClearPreferredPosForAxis(ImGuiAxis_X);
6993
NavMoveRequestCancel();
6994
}
6995
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?
6996
{
6997
toggled = true;
6998
NavClearPreferredPosForAxis(ImGuiAxis_X);
6999
NavMoveRequestCancel();
7000
}
7001
7002
if (toggled)
7003
{
7004
is_open = !is_open;
7005
window->DC.StateStorage->SetInt(storage_id, is_open);
7006
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
7007
}
7008
}
7009
7010
// Multi-selection support (footer)
7011
if (is_multi_select)
7012
{
7013
bool pressed_copy = pressed && !toggled;
7014
MultiSelectItemFooter(id, &selected, &pressed_copy);
7015
if (pressed)
7016
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
7017
}
7018
7019
if (selected != was_selected)
7020
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7021
7022
// Render
7023
{
7024
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
7025
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
7026
if (is_multi_select)
7027
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7028
if (display_frame)
7029
{
7030
// Framed type
7031
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7032
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
7033
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
7034
if (span_all_columns && !span_all_columns_label)
7035
TablePopBackgroundChannel();
7036
if (flags & ImGuiTreeNodeFlags_Bullet)
7037
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
7038
else if (!is_leaf)
7039
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);
7040
else // Leaf without bullet, left-adjusted text
7041
text_pos.x -= text_offset_x - padding.x;
7042
if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
7043
frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
7044
if (g.LogEnabled)
7045
LogSetNextTextDecoration("###", "###");
7046
}
7047
else
7048
{
7049
// Unframed typed for tree nodes
7050
if (hovered || selected)
7051
{
7052
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7053
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
7054
}
7055
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
7056
if (span_all_columns && !span_all_columns_label)
7057
TablePopBackgroundChannel();
7058
if (flags & ImGuiTreeNodeFlags_Bullet)
7059
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
7060
else if (!is_leaf)
7061
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);
7062
if (g.LogEnabled)
7063
LogSetNextTextDecoration(">", NULL);
7064
}
7065
7066
if (draw_tree_lines)
7067
TreeNodeDrawLineToChildNode(ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.5f));
7068
7069
// Label
7070
if (display_frame)
7071
RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
7072
else
7073
RenderText(text_pos, label, label_end, false);
7074
7075
if (span_all_columns_label)
7076
TablePopBackgroundChannel();
7077
}
7078
7079
if (is_open && store_tree_node_stack_data)
7080
TreeNodeStoreStackData(flags, text_pos.x - text_offset_x); // Call before TreePushOverrideID()
7081
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
7082
TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
7083
7084
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
7085
return is_open;
7086
}
7087
7088
// Draw horizontal line from our parent node
7089
// This is only called for visible child nodes so we are not too fussy anymore about performances
7090
void ImGui::TreeNodeDrawLineToChildNode(const ImVec2& target_pos)
7091
{
7092
ImGuiContext& g = *GImGui;
7093
ImGuiWindow* window = g.CurrentWindow;
7094
if (window->DC.TreeDepth == 0 || (window->DC.TreeHasStackDataDepthMask & (1 << (window->DC.TreeDepth - 1))) == 0)
7095
return;
7096
7097
ImGuiTreeNodeStackData* parent_data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7098
float x1 = ImTrunc(parent_data->DrawLinesX1);
7099
float x2 = ImTrunc(target_pos.x - g.Style.ItemInnerSpacing.x);
7100
float y = ImTrunc(target_pos.y);
7101
float rounding = (g.Style.TreeLinesRounding > 0.0f) ? ImMin(x2 - x1, g.Style.TreeLinesRounding) : 0.0f;
7102
parent_data->DrawLinesToNodesY2 = ImMax(parent_data->DrawLinesToNodesY2, y - rounding);
7103
if (x1 >= x2)
7104
return;
7105
if (rounding > 0.0f)
7106
{
7107
x1 += 0.5f + rounding;
7108
window->DrawList->PathArcToFast(ImVec2(x1, y - rounding), rounding, 6, 3);
7109
if (x1 < x2)
7110
window->DrawList->PathLineTo(ImVec2(x2, y));
7111
window->DrawList->PathStroke(GetColorU32(ImGuiCol_TreeLines), ImDrawFlags_None, g.Style.TreeLinesSize);
7112
}
7113
else
7114
{
7115
window->DrawList->AddLine(ImVec2(x1, y), ImVec2(x2, y), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7116
}
7117
}
7118
7119
// Draw vertical line of the hierarchy
7120
void ImGui::TreeNodeDrawLineToTreePop(const ImGuiTreeNodeStackData* data)
7121
{
7122
ImGuiContext& g = *GImGui;
7123
ImGuiWindow* window = g.CurrentWindow;
7124
float y1 = ImMax(data->NavRect.Max.y, window->ClipRect.Min.y);
7125
float y2 = data->DrawLinesToNodesY2;
7126
if (data->TreeFlags & ImGuiTreeNodeFlags_DrawLinesFull)
7127
{
7128
float y2_full = window->DC.CursorPos.y;
7129
if (g.CurrentTable)
7130
y2_full = ImMax(g.CurrentTable->RowPosY2, y2_full);
7131
y2_full = ImTrunc(y2_full - g.Style.ItemSpacing.y - g.FontSize * 0.5f);
7132
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
7133
y2 = y2_full;
7134
}
7135
y2 = ImMin(y2, window->ClipRect.Max.y);
7136
if (y2 <= y1)
7137
return;
7138
float x = ImTrunc(data->DrawLinesX1);
7139
if (data->DrawLinesTableColumn != -1)
7140
TablePushColumnChannel(data->DrawLinesTableColumn);
7141
window->DrawList->AddLine(ImVec2(x, y1), ImVec2(x, y2), GetColorU32(ImGuiCol_TreeLines), g.Style.TreeLinesSize);
7142
if (data->DrawLinesTableColumn != -1)
7143
TablePopColumnChannel();
7144
}
7145
7146
void ImGui::TreePush(const char* str_id)
7147
{
7148
ImGuiWindow* window = GetCurrentWindow();
7149
Indent();
7150
window->DC.TreeDepth++;
7151
PushID(str_id);
7152
}
7153
7154
void ImGui::TreePush(const void* ptr_id)
7155
{
7156
ImGuiWindow* window = GetCurrentWindow();
7157
Indent();
7158
window->DC.TreeDepth++;
7159
PushID(ptr_id);
7160
}
7161
7162
void ImGui::TreePushOverrideID(ImGuiID id)
7163
{
7164
ImGuiContext& g = *GImGui;
7165
ImGuiWindow* window = g.CurrentWindow;
7166
Indent();
7167
window->DC.TreeDepth++;
7168
PushOverrideID(id);
7169
}
7170
7171
void ImGui::TreePop()
7172
{
7173
ImGuiContext& g = *GImGui;
7174
ImGuiWindow* window = g.CurrentWindow;
7175
Unindent();
7176
7177
window->DC.TreeDepth--;
7178
ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
7179
7180
if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask)
7181
{
7182
const ImGuiTreeNodeStackData* data = &g.TreeNodeStack.Data[g.TreeNodeStack.Size - 1];
7183
IM_ASSERT(data->ID == window->IDStack.back());
7184
7185
// Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsToParent is enabled)
7186
if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsToParent)
7187
if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
7188
NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
7189
7190
// Draw hierarchy lines
7191
if (data->DrawLinesX1 != +FLT_MAX && window->DC.CursorPos.y >= window->ClipRect.Min.y)
7192
TreeNodeDrawLineToTreePop(data);
7193
7194
g.TreeNodeStack.pop_back();
7195
window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
7196
window->DC.TreeRecordsClippedNodesY2Mask &= ~tree_depth_mask;
7197
}
7198
7199
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.
7200
PopID();
7201
}
7202
7203
// Horizontal distance preceding label when using TreeNode() or Bullet()
7204
float ImGui::GetTreeNodeToLabelSpacing()
7205
{
7206
ImGuiContext& g = *GImGui;
7207
return g.FontSize + (g.Style.FramePadding.x * 2.0f);
7208
}
7209
7210
// Set next TreeNode/CollapsingHeader open state.
7211
void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
7212
{
7213
ImGuiContext& g = *GImGui;
7214
if (g.CurrentWindow->SkipItems)
7215
return;
7216
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
7217
g.NextItemData.OpenVal = is_open;
7218
g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
7219
}
7220
7221
// Set next TreeNode/CollapsingHeader storage id.
7222
void ImGui::SetNextItemStorageID(ImGuiID storage_id)
7223
{
7224
ImGuiContext& g = *GImGui;
7225
if (g.CurrentWindow->SkipItems)
7226
return;
7227
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
7228
g.NextItemData.StorageId = storage_id;
7229
}
7230
7231
// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
7232
// 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().
7233
bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
7234
{
7235
ImGuiWindow* window = GetCurrentWindow();
7236
if (window->SkipItems)
7237
return false;
7238
ImGuiID id = window->GetID(label);
7239
return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
7240
}
7241
7242
// p_visible == NULL : regular collapsing header
7243
// 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
7244
// p_visible != NULL && *p_visible == false : do not show the header at all
7245
// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
7246
bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
7247
{
7248
ImGuiWindow* window = GetCurrentWindow();
7249
if (window->SkipItems)
7250
return false;
7251
7252
if (p_visible && !*p_visible)
7253
return false;
7254
7255
ImGuiID id = window->GetID(label);
7256
flags |= ImGuiTreeNodeFlags_CollapsingHeader;
7257
if (p_visible)
7258
flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
7259
bool is_open = TreeNodeBehavior(id, flags, label);
7260
if (p_visible != NULL)
7261
{
7262
// Create a small overlapping close button
7263
// FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
7264
// FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
7265
ImGuiContext& g = *GImGui;
7266
ImGuiLastItemData last_item_backup = g.LastItemData;
7267
float button_size = g.FontSize;
7268
float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
7269
float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
7270
ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
7271
if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
7272
*p_visible = false;
7273
g.LastItemData = last_item_backup;
7274
}
7275
7276
return is_open;
7277
}
7278
7279
//-------------------------------------------------------------------------
7280
// [SECTION] Widgets: Selectable
7281
//-------------------------------------------------------------------------
7282
// - Selectable()
7283
//-------------------------------------------------------------------------
7284
7285
// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
7286
// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
7287
// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
7288
// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
7289
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7290
{
7291
ImGuiWindow* window = GetCurrentWindow();
7292
if (window->SkipItems)
7293
return false;
7294
7295
ImGuiContext& g = *GImGui;
7296
const ImGuiStyle& style = g.Style;
7297
7298
// Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
7299
ImGuiID id = window->GetID(label);
7300
ImVec2 label_size = CalcTextSize(label, NULL, true);
7301
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
7302
ImVec2 pos = window->DC.CursorPos;
7303
pos.y += window->DC.CurrLineTextBaseOffset;
7304
ItemSize(size, 0.0f);
7305
7306
// Fill horizontal space
7307
// We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
7308
const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
7309
const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
7310
const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
7311
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
7312
size.x = ImMax(label_size.x, max_x - min_x);
7313
7314
// Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
7315
// FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
7316
ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y);
7317
if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
7318
{
7319
const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
7320
const float spacing_y = style.ItemSpacing.y;
7321
const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
7322
const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
7323
bb.Min.x -= spacing_L;
7324
bb.Min.y -= spacing_U;
7325
bb.Max.x += (spacing_x - spacing_L);
7326
bb.Max.y += (spacing_y - spacing_U);
7327
}
7328
//if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
7329
7330
const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
7331
const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
7332
bool is_visible;
7333
if (span_all_columns)
7334
{
7335
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
7336
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
7337
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
7338
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
7339
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
7340
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7341
window->ClipRect.Min.x = backup_clip_rect_min_x;
7342
window->ClipRect.Max.x = backup_clip_rect_max_x;
7343
}
7344
else
7345
{
7346
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
7347
}
7348
7349
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
7350
if (!is_visible)
7351
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)
7352
return false;
7353
7354
const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
7355
if (disabled_item && !disabled_global) // Only testing this as an optimization
7356
BeginDisabled();
7357
7358
// FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
7359
// which would be advantageous since most selectable are not selected.
7360
if (span_all_columns)
7361
{
7362
if (g.CurrentTable)
7363
TablePushBackgroundChannel();
7364
else if (window->DC.CurrentColumns)
7365
PushColumnsBackground();
7366
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
7367
g.LastItemData.ClipRect = window->ClipRect;
7368
}
7369
7370
// We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
7371
ImGuiButtonFlags button_flags = 0;
7372
if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
7373
if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
7374
if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
7375
if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
7376
if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
7377
if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
7378
7379
// Multi-selection support (header)
7380
const bool was_selected = selected;
7381
if (is_multi_select)
7382
{
7383
// Handle multi-select + alter button flags for it
7384
MultiSelectItemHeader(id, &selected, &button_flags);
7385
}
7386
7387
bool hovered, held;
7388
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
7389
bool auto_selected = false;
7390
7391
// Multi-selection support (footer)
7392
if (is_multi_select)
7393
{
7394
MultiSelectItemFooter(id, &selected, &pressed);
7395
}
7396
else
7397
{
7398
// Auto-select when moved into
7399
// - This will be more fully fleshed in the range-select branch
7400
// - This is not exposed as it won't nicely work with some user side handling of shift/control
7401
// - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7402
// - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7403
// - (2) usage will fail with clipped items
7404
// The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7405
if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7406
if (g.NavJustMovedToId == id && (g.NavJustMovedToKeyMods & ImGuiMod_Ctrl) == 0)
7407
selected = pressed = auto_selected = true;
7408
}
7409
7410
// Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7411
if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7412
{
7413
if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7414
{
7415
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7416
if (g.IO.ConfigNavCursorVisibleAuto)
7417
g.NavCursorVisible = false;
7418
}
7419
}
7420
if (pressed)
7421
MarkItemEdited(id);
7422
7423
if (selected != was_selected)
7424
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7425
7426
// Render
7427
if (is_visible)
7428
{
7429
const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7430
if (highlighted || selected)
7431
{
7432
// Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7433
ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7434
RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
7435
}
7436
if (g.NavId == id)
7437
{
7438
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7439
if (is_multi_select)
7440
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7441
RenderNavCursor(bb, id, nav_render_cursor_flags);
7442
}
7443
}
7444
7445
if (span_all_columns)
7446
{
7447
if (g.CurrentTable)
7448
TablePopBackgroundChannel();
7449
else if (window->DC.CurrentColumns)
7450
PopColumnsBackground();
7451
}
7452
7453
// Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns.
7454
if (is_visible)
7455
RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb);
7456
7457
// Automatically close popups
7458
if (pressed && !auto_selected && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7459
CloseCurrentPopup();
7460
7461
if (disabled_item && !disabled_global)
7462
EndDisabled();
7463
7464
// Selectable() always returns a pressed state!
7465
// Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7466
// selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7467
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7468
return pressed; //-V1020
7469
}
7470
7471
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7472
{
7473
if (Selectable(label, *p_selected, flags, size_arg))
7474
{
7475
*p_selected = !*p_selected;
7476
return true;
7477
}
7478
return false;
7479
}
7480
7481
7482
//-------------------------------------------------------------------------
7483
// [SECTION] Widgets: Typing-Select support
7484
//-------------------------------------------------------------------------
7485
7486
// [Experimental] Currently not exposed in public API.
7487
// Consume character inputs and return search request, if any.
7488
// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7489
// if (ImGui::IsWindowFocused(...))
7490
// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7491
// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7492
// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7493
ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7494
{
7495
ImGuiContext& g = *GImGui;
7496
ImGuiTypingSelectState* data = &g.TypingSelectState;
7497
ImGuiTypingSelectRequest* out_request = &data->Request;
7498
7499
// Clear buffer
7500
const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7501
const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7502
if (data->SearchBuffer[0] != 0)
7503
{
7504
bool clear_buffer = false;
7505
clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7506
clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7507
clear_buffer |= g.NavAnyRequest;
7508
clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7509
clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7510
clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7511
//if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7512
if (clear_buffer)
7513
data->Clear();
7514
}
7515
7516
// Append to buffer
7517
const int buffer_max_len = IM_COUNTOF(data->SearchBuffer) - 1;
7518
int buffer_len = (int)ImStrlen(data->SearchBuffer);
7519
bool select_request = false;
7520
for (ImWchar w : g.IO.InputQueueCharacters)
7521
{
7522
const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7523
if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7524
continue;
7525
char w_buf[5];
7526
ImTextCharToUtf8(w_buf, (unsigned int)w);
7527
if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7528
{
7529
select_request = true; // Same character: don't need to append to buffer.
7530
continue;
7531
}
7532
if (data->SingleCharModeLock)
7533
{
7534
data->Clear(); // Different character: clear
7535
buffer_len = 0;
7536
}
7537
memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7538
buffer_len += w_len;
7539
select_request = true;
7540
}
7541
g.IO.InputQueueCharacters.resize(0);
7542
7543
// Handle backspace
7544
if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7545
{
7546
char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7547
*p = 0;
7548
buffer_len = (int)(p - data->SearchBuffer);
7549
}
7550
7551
// Return request if any
7552
if (buffer_len == 0)
7553
return NULL;
7554
if (select_request)
7555
{
7556
data->FocusScope = g.NavFocusScopeId;
7557
data->LastRequestFrame = g.FrameCount;
7558
data->LastRequestTime = (float)g.Time;
7559
}
7560
out_request->Flags = flags;
7561
out_request->SearchBufferLen = buffer_len;
7562
out_request->SearchBuffer = data->SearchBuffer;
7563
out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7564
out_request->SingleCharMode = false;
7565
out_request->SingleCharSize = 0;
7566
7567
// Calculate if buffer contains the same character repeated.
7568
// - This can be used to implement a special search mode on first character.
7569
// - Performed on UTF-8 codepoint for correctness.
7570
// - SingleCharMode is always set for first input character, because it usually leads to a "next".
7571
if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7572
{
7573
const char* buf_begin = out_request->SearchBuffer;
7574
const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7575
const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7576
const char* p = buf_begin + c0_len;
7577
for (; p < buf_end; p += c0_len)
7578
if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7579
break;
7580
const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7581
out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7582
out_request->SingleCharSize = (ImS8)c0_len;
7583
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.
7584
}
7585
7586
return out_request;
7587
}
7588
7589
static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7590
{
7591
int match_len = 0;
7592
while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7593
match_len++;
7594
return match_len;
7595
}
7596
7597
// Default handler for finding a result for typing-select. You may implement your own.
7598
// You might want to display a tooltip to visualize the current request SearchBuffer
7599
// When SingleCharMode is set:
7600
// - it is better to NOT display a tooltip of other on-screen display indicator.
7601
// - the index of the currently focused item is required.
7602
// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7603
int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7604
{
7605
if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7606
return -1;
7607
int idx = -1;
7608
if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7609
idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7610
else
7611
idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7612
if (idx != -1)
7613
SetNavCursorVisibleAfterMove();
7614
return idx;
7615
}
7616
7617
// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7618
int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7619
{
7620
// FIXME: Assume selection user data is index. Would be extremely practical.
7621
//if (nav_item_idx == -1)
7622
// nav_item_idx = (int)g.NavLastValidSelectionUserData;
7623
7624
int first_match_idx = -1;
7625
bool return_next_match = false;
7626
for (int idx = 0; idx < items_count; idx++)
7627
{
7628
const char* item_name = get_item_name_func(user_data, idx);
7629
if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7630
continue;
7631
if (return_next_match) // Return next matching item after current item.
7632
return idx;
7633
if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7634
return idx;
7635
if (first_match_idx == -1) // Record first match for wrapping.
7636
first_match_idx = idx;
7637
if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7638
return_next_match = true;
7639
}
7640
return first_match_idx; // First result
7641
}
7642
7643
int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7644
{
7645
int longest_match_idx = -1;
7646
int longest_match_len = 0;
7647
for (int idx = 0; idx < items_count; idx++)
7648
{
7649
const char* item_name = get_item_name_func(user_data, idx);
7650
const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7651
if (match_len <= longest_match_len)
7652
continue;
7653
longest_match_idx = idx;
7654
longest_match_len = match_len;
7655
if (match_len == req->SearchBufferLen)
7656
break;
7657
}
7658
return longest_match_idx;
7659
}
7660
7661
void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7662
{
7663
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7664
Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7665
Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7666
Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7667
#else
7668
IM_UNUSED(data);
7669
#endif
7670
}
7671
7672
//-------------------------------------------------------------------------
7673
// [SECTION] Widgets: Box-Select support
7674
// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7675
//-------------------------------------------------------------------------
7676
// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7677
//-------------------------------------------------------------------------
7678
// - BoxSelectPreStartDrag() [Internal]
7679
// - BoxSelectActivateDrag() [Internal]
7680
// - BoxSelectDeactivateDrag() [Internal]
7681
// - BoxSelectScrollWithMouseDrag() [Internal]
7682
// - BeginBoxSelect() [Internal]
7683
// - EndBoxSelect() [Internal]
7684
//-------------------------------------------------------------------------
7685
7686
// Call on the initial click.
7687
static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7688
{
7689
ImGuiContext& g = *GImGui;
7690
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7691
bs->ID = id;
7692
bs->IsStarting = true; // Consider starting box-select.
7693
bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7694
bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7695
bs->KeyMods = g.IO.KeyMods;
7696
bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7697
bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7698
}
7699
7700
static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7701
{
7702
ImGuiContext& g = *GImGui;
7703
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7704
bs->IsActive = true;
7705
bs->Window = window;
7706
bs->IsStarting = false;
7707
ImGui::SetActiveID(bs->ID, window);
7708
ImGui::SetActiveIdUsingAllKeyboardKeys();
7709
if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7710
bs->RequestClear = true;
7711
}
7712
7713
static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7714
{
7715
ImGuiContext& g = *GImGui;
7716
bs->IsActive = bs->IsStarting = false;
7717
if (g.ActiveId == bs->ID)
7718
{
7719
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7720
ImGui::ClearActiveID();
7721
}
7722
bs->ID = 0;
7723
}
7724
7725
static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7726
{
7727
ImGuiContext& g = *GImGui;
7728
IM_ASSERT(bs->Window == window);
7729
for (int n = 0; n < 2; n++) // each axis
7730
{
7731
const float mouse_pos = g.IO.MousePos[n];
7732
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;
7733
const float scroll_curr = window->Scroll[n];
7734
if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7735
continue;
7736
7737
const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7738
const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7739
bs->ScrollAccum[n] += scroll_step;
7740
7741
// Accumulate into a stored value so we can handle high-framerate
7742
const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7743
if (scroll_step_i == 0.0f)
7744
continue;
7745
if (n == 0)
7746
ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7747
else
7748
ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7749
bs->ScrollAccum[n] -= scroll_step_i;
7750
}
7751
}
7752
7753
bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7754
{
7755
ImGuiContext& g = *GImGui;
7756
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7757
KeepAliveID(box_select_id);
7758
if (bs->ID != box_select_id)
7759
return false;
7760
7761
// IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7762
bs->UnclipMode = false;
7763
bs->RequestClear = false;
7764
if (bs->IsStarting && IsMouseDragPastThreshold(0))
7765
BoxSelectActivateDrag(bs, window);
7766
else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7767
BoxSelectDeactivateDrag(bs);
7768
if (!bs->IsActive)
7769
return false;
7770
7771
// Current frame absolute prev/current rectangles are used to toggle selection.
7772
// They are derived from positions relative to scrolling space.
7773
ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7774
ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7775
ImVec2 curr_end_pos_abs = g.IO.MousePos;
7776
if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7777
curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7778
bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7779
bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7780
bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7781
bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7782
7783
// Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7784
// Storing an extra rect used by widgets supporting box-select.
7785
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7786
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7787
{
7788
bs->UnclipMode = true;
7789
bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7790
bs->UnclipRect.Add(bs->BoxSelectRectCurr);
7791
}
7792
7793
//GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7794
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7795
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7796
return true;
7797
}
7798
7799
void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7800
{
7801
ImGuiContext& g = *GImGui;
7802
ImGuiWindow* window = g.CurrentWindow;
7803
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7804
IM_ASSERT(bs->IsActive);
7805
bs->UnclipMode = false;
7806
7807
// Render selection rectangle
7808
bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7809
ImRect box_select_r = bs->BoxSelectRectCurr;
7810
box_select_r.ClipWith(scope_rect);
7811
window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7812
window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT FIXME-DPI: Styling
7813
7814
// Scroll
7815
const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7816
if (enable_scroll)
7817
{
7818
ImRect scroll_r = scope_rect;
7819
scroll_r.Expand(-g.FontSize);
7820
//GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7821
if (!scroll_r.Contains(g.IO.MousePos))
7822
BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7823
}
7824
}
7825
7826
//-------------------------------------------------------------------------
7827
// [SECTION] Widgets: Multi-Select support
7828
//-------------------------------------------------------------------------
7829
// - DebugLogMultiSelectRequests() [Internal]
7830
// - CalcScopeRect() [Internal]
7831
// - BeginMultiSelect()
7832
// - EndMultiSelect()
7833
// - SetNextItemSelectionUserData()
7834
// - MultiSelectItemHeader() [Internal]
7835
// - MultiSelectItemFooter() [Internal]
7836
// - DebugNodeMultiSelectState() [Internal]
7837
//-------------------------------------------------------------------------
7838
7839
static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7840
{
7841
ImGuiContext& g = *GImGui;
7842
IM_UNUSED(function);
7843
for (const ImGuiSelectionRequest& req : io->Requests)
7844
{
7845
if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7846
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);
7847
}
7848
}
7849
7850
static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7851
{
7852
ImGuiContext& g = *GImGui;
7853
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7854
{
7855
// Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7856
return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
7857
}
7858
else
7859
{
7860
// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7861
ImRect scope_rect = window->InnerClipRect;
7862
if (g.CurrentTable != NULL)
7863
scope_rect = g.CurrentTable->HostClipRect;
7864
7865
// Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7866
scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
7867
return scope_rect;
7868
}
7869
}
7870
7871
// Return ImGuiMultiSelectIO structure.
7872
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7873
// Passing 'selection_size' and 'items_count' parameters is currently optional.
7874
// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7875
// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7876
// - '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).
7877
// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7878
// - 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.
7879
ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7880
{
7881
ImGuiContext& g = *GImGui;
7882
ImGuiWindow* window = g.CurrentWindow;
7883
7884
if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7885
g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
7886
ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7887
IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7888
g.CurrentMultiSelect = ms;
7889
if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7890
flags |= ImGuiMultiSelectFlags_ScopeWindow;
7891
if (flags & ImGuiMultiSelectFlags_SingleSelect)
7892
flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7893
if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7894
flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7895
7896
// FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250)
7897
// They should perhaps be stacked properly?
7898
if (ImGuiTable* table = g.CurrentTable)
7899
if (table->CurrentColumn != -1)
7900
TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it.
7901
7902
// FIXME: BeginFocusScope()
7903
const ImGuiID id = window->IDStack.back();
7904
ms->Clear();
7905
ms->FocusScopeId = id;
7906
ms->Flags = flags;
7907
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7908
ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7909
ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7910
PushFocusScope(ms->FocusScopeId);
7911
if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7912
window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7913
7914
// Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7915
ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7916
if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7917
ms->KeyMods &= ~ImGuiMod_Shift;
7918
7919
// Bind storage
7920
ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
7921
storage->ID = id;
7922
storage->LastFrameActive = g.FrameCount;
7923
storage->LastSelectionSize = selection_size;
7924
storage->Window = window;
7925
ms->Storage = storage;
7926
7927
// Output to user
7928
ms->IO.Requests.resize(0);
7929
ms->IO.RangeSrcItem = storage->RangeSrcItem;
7930
ms->IO.NavIdItem = storage->NavIdItem;
7931
ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7932
ms->IO.ItemsCount = items_count;
7933
7934
// Clear when using Navigation to move within the scope
7935
// (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7936
bool request_clear = false;
7937
bool request_select_all = false;
7938
if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7939
{
7940
if (ms->KeyMods & ImGuiMod_Shift)
7941
ms->IsKeyboardSetRange = true;
7942
if (ms->IsKeyboardSetRange)
7943
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7944
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7945
request_clear = true;
7946
}
7947
else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7948
{
7949
// Also clear on leaving scope (may be optional?)
7950
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7951
request_clear = true;
7952
}
7953
7954
// Box-select handling: update active state.
7955
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7956
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7957
{
7958
ms->BoxSelectId = GetID("##BoxSelect");
7959
if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
7960
request_clear |= bs->RequestClear;
7961
}
7962
7963
if (ms->IsFocused)
7964
{
7965
// Shortcut: Clear selection (Escape)
7966
// - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7967
// - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7968
if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7969
{
7970
if (selection_size != 0 || bs->IsActive)
7971
if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
7972
{
7973
request_clear = true;
7974
if (bs->IsActive)
7975
BoxSelectDeactivateDrag(bs);
7976
}
7977
}
7978
7979
// Shortcut: Select all (Ctrl+A)
7980
if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7981
if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
7982
request_select_all = true;
7983
}
7984
7985
if (request_clear || request_select_all)
7986
{
7987
MultiSelectAddSetAll(ms, request_select_all);
7988
if (!request_select_all)
7989
storage->LastSelectionSize = 0;
7990
}
7991
ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7992
ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7993
7994
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7995
DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
7996
7997
return &ms->IO;
7998
}
7999
8000
// Return updated ImGuiMultiSelectIO structure.
8001
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
8002
ImGuiMultiSelectIO* ImGui::EndMultiSelect()
8003
{
8004
ImGuiContext& g = *GImGui;
8005
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8006
ImGuiMultiSelectState* storage = ms->Storage;
8007
ImGuiWindow* window = g.CurrentWindow;
8008
IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
8009
IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
8010
IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
8011
8012
ImRect scope_rect = CalcScopeRect(ms, window);
8013
if (ms->IsFocused)
8014
{
8015
// We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
8016
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)
8017
{
8018
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
8019
storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
8020
}
8021
if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
8022
{
8023
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
8024
storage->NavIdItem = ImGuiSelectionUserData_Invalid;
8025
storage->NavIdSelected = -1;
8026
}
8027
8028
if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
8029
EndBoxSelect(scope_rect, ms->Flags);
8030
}
8031
8032
if (ms->IsEndIO == false)
8033
ms->IO.Requests.resize(0);
8034
8035
// Clear selection when clicking void?
8036
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
8037
// The InnerRect test is necessary for non-child/decorated windows.
8038
bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos);
8039
if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
8040
scope_hovered &= scope_rect.Contains(g.IO.MousePos);
8041
if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
8042
{
8043
if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8044
{
8045
if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
8046
{
8047
BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
8048
FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
8049
SetHoveredID(ms->BoxSelectId);
8050
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
8051
SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
8052
}
8053
}
8054
8055
if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
8056
if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
8057
MultiSelectAddSetAll(ms, false);
8058
}
8059
8060
// Courtesy nav wrapping helper flag
8061
if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
8062
{
8063
IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
8064
ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
8065
}
8066
8067
// Unwind
8068
window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
8069
PopFocusScope();
8070
8071
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
8072
DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
8073
8074
ms->FocusScopeId = 0;
8075
ms->Flags = ImGuiMultiSelectFlags_None;
8076
g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
8077
8078
return &ms->IO;
8079
}
8080
8081
void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
8082
{
8083
// Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
8084
// This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
8085
ImGuiContext& g = *GImGui;
8086
g.NextItemData.SelectionUserData = selection_user_data;
8087
g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
8088
8089
if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
8090
{
8091
// Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
8092
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
8093
if (ms->IO.RangeSrcItem == selection_user_data)
8094
ms->RangeSrcPassedBy = true;
8095
}
8096
else
8097
{
8098
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
8099
}
8100
}
8101
8102
// In charge of:
8103
// - Applying SetAll for submitted items.
8104
// - Applying SetRange for submitted items and record end points.
8105
// - Altering button behavior flags to facilitate use with drag and drop.
8106
void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
8107
{
8108
ImGuiContext& g = *GImGui;
8109
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8110
8111
bool selected = *p_selected;
8112
if (ms->IsFocused)
8113
{
8114
ImGuiMultiSelectState* storage = ms->Storage;
8115
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8116
IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
8117
8118
// Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
8119
// This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
8120
// If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
8121
if (ms->LoopRequestSetAll != -1)
8122
selected = (ms->LoopRequestSetAll == 1);
8123
8124
// 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)
8125
// For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
8126
if (ms->IsKeyboardSetRange)
8127
{
8128
IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
8129
const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
8130
if (is_range_dst)
8131
ms->RangeDstPassedBy = true;
8132
if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
8133
{
8134
storage->RangeSrcItem = item_data;
8135
storage->RangeSelected = selected ? 1 : 0;
8136
}
8137
const bool is_range_src = storage->RangeSrcItem == item_data;
8138
if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
8139
{
8140
// Apply range-select value to visible items
8141
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
8142
selected = (storage->RangeSelected != 0);
8143
}
8144
else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8145
{
8146
// Clear other items
8147
selected = false;
8148
}
8149
}
8150
*p_selected = selected;
8151
}
8152
8153
// Alter button behavior flags
8154
// To handle drag and drop of multiple items we need to avoid clearing selection on click.
8155
// 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.
8156
if (p_button_flags != NULL)
8157
{
8158
ImGuiButtonFlags button_flags = *p_button_flags;
8159
button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
8160
if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
8161
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
8162
else
8163
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
8164
*p_button_flags = button_flags;
8165
}
8166
}
8167
8168
// In charge of:
8169
// - Auto-select on navigation.
8170
// - Box-select toggle handling.
8171
// - Right-click handling.
8172
// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
8173
// - Record current selection state for RangeSrc
8174
// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
8175
void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
8176
{
8177
ImGuiContext& g = *GImGui;
8178
ImGuiWindow* window = g.CurrentWindow;
8179
8180
bool selected = *p_selected;
8181
bool pressed = *p_pressed;
8182
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
8183
ImGuiMultiSelectState* storage = ms->Storage;
8184
if (pressed)
8185
ms->IsFocused = true;
8186
8187
bool hovered = false;
8188
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
8189
hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
8190
if (!ms->IsFocused && !hovered)
8191
return;
8192
8193
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
8194
8195
ImGuiMultiSelectFlags flags = ms->Flags;
8196
const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
8197
bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
8198
bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
8199
8200
bool apply_to_range_src = false;
8201
8202
if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8203
apply_to_range_src = true;
8204
if (ms->IsEndIO == false)
8205
{
8206
ms->IO.Requests.resize(0);
8207
ms->IsEndIO = true;
8208
}
8209
8210
// Auto-select as you navigate a list
8211
if (g.NavJustMovedToId == id)
8212
{
8213
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8214
{
8215
if (is_ctrl && is_shift)
8216
pressed = true;
8217
else if (!is_ctrl)
8218
selected = pressed = true;
8219
}
8220
else
8221
{
8222
// With NoAutoSelect, using Shift+keyboard performs a write/copy
8223
if (is_shift)
8224
pressed = true;
8225
else if (!is_ctrl)
8226
apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
8227
}
8228
}
8229
8230
if (apply_to_range_src)
8231
{
8232
storage->RangeSrcItem = item_data;
8233
storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
8234
}
8235
8236
// Box-select toggle handling
8237
if (ms->BoxSelectId != 0)
8238
if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
8239
{
8240
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect);
8241
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect);
8242
if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
8243
{
8244
if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
8245
{
8246
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)
8247
bs->IsStartedSetNavIdOnce = false;
8248
}
8249
else
8250
{
8251
selected = !selected;
8252
MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
8253
}
8254
storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
8255
}
8256
}
8257
8258
// Right-click handling.
8259
// FIXME-MULTISELECT: Maybe should be moved to Selectable()? Also see #5816, #8200, #9015
8260
if (hovered && IsMouseClicked(1) && (flags & (ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoSelectOnRightClick)) == 0)
8261
{
8262
if (g.ActiveId != 0 && g.ActiveId != id)
8263
ClearActiveID();
8264
SetFocusID(id, window);
8265
if (!pressed && !selected)
8266
{
8267
pressed = true;
8268
is_ctrl = is_shift = false;
8269
}
8270
}
8271
8272
// Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
8273
// The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
8274
// (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
8275
const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
8276
8277
// Alter selection
8278
if (pressed && (!enter_pressed || !selected))
8279
{
8280
// Box-select
8281
ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
8282
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
8283
if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
8284
BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
8285
8286
//----------------------------------------------------------------------------------------
8287
// ACTION | Begin | Pressed/Activated | End
8288
//----------------------------------------------------------------------------------------
8289
// Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
8290
// Keys Navigated: Ctrl | n/a | n/a
8291
// Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8292
// Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
8293
// Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
8294
// Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
8295
// Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
8296
//----------------------------------------------------------------------------------------
8297
// Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
8298
// Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
8299
// Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
8300
// Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
8301
//----------------------------------------------------------------------------------------
8302
8303
if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
8304
{
8305
bool request_clear = false;
8306
if (is_singleselect)
8307
request_clear = true;
8308
else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
8309
request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
8310
else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
8311
request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
8312
if (request_clear)
8313
MultiSelectAddSetAll(ms, false);
8314
}
8315
8316
int range_direction;
8317
bool range_selected;
8318
if (is_shift && !is_singleselect)
8319
{
8320
//IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
8321
if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
8322
storage->RangeSrcItem = item_data;
8323
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8324
{
8325
// Shift+Arrow always select
8326
// Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
8327
range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8328
}
8329
else
8330
{
8331
// Shift+Arrow copy source selection state
8332
// Shift+Click always copy from target selection state
8333
if (ms->IsKeyboardSetRange)
8334
range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
8335
else
8336
range_selected = !selected;
8337
}
8338
range_direction = ms->RangeSrcPassedBy ? +1 : -1;
8339
}
8340
else
8341
{
8342
// Ctrl inverts selection, otherwise always select
8343
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
8344
selected = is_ctrl ? !selected : true;
8345
else
8346
selected = !selected;
8347
storage->RangeSrcItem = item_data;
8348
range_selected = selected;
8349
range_direction = +1;
8350
}
8351
MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
8352
}
8353
8354
// Update/store the selection state of the Source item (used by Ctrl+Shift, when Source is unselected we perform a range unselect)
8355
if (storage->RangeSrcItem == item_data)
8356
storage->RangeSelected = selected ? 1 : 0;
8357
8358
// Update/store the selection state of focused item
8359
if (g.NavId == id)
8360
{
8361
storage->NavIdItem = item_data;
8362
storage->NavIdSelected = selected ? 1 : 0;
8363
}
8364
if (storage->NavIdItem == item_data)
8365
ms->NavIdPassedBy = true;
8366
ms->LastSubmittedItem = item_data;
8367
8368
*p_selected = selected;
8369
*p_pressed = pressed;
8370
}
8371
8372
void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
8373
{
8374
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
8375
ms->IO.Requests.resize(0); // Can always clear previous requests
8376
ms->IO.Requests.push_back(req); // Add new request
8377
}
8378
8379
void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
8380
{
8381
// Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
8382
if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
8383
{
8384
ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
8385
if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
8386
{
8387
prev->RangeLastItem = last_item;
8388
return;
8389
}
8390
}
8391
8392
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
8393
ms->IO.Requests.push_back(req); // Add new request
8394
}
8395
8396
void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
8397
{
8398
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8399
const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8400
if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8401
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*");
8402
if (!is_active) { PopStyleColor(); }
8403
if (!open)
8404
return;
8405
Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8406
Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8407
Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8408
TreePop();
8409
#else
8410
IM_UNUSED(storage);
8411
#endif
8412
}
8413
8414
//-------------------------------------------------------------------------
8415
// [SECTION] Widgets: Multi-Select helpers
8416
//-------------------------------------------------------------------------
8417
// - ImGuiSelectionBasicStorage
8418
// - ImGuiSelectionExternalStorage
8419
//-------------------------------------------------------------------------
8420
8421
ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8422
{
8423
Size = 0;
8424
PreserveOrder = false;
8425
UserData = NULL;
8426
AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8427
_SelectionOrder = 1; // Always >0
8428
}
8429
8430
void ImGuiSelectionBasicStorage::Clear()
8431
{
8432
Size = 0;
8433
_SelectionOrder = 1; // Always >0
8434
_Storage.Data.resize(0);
8435
}
8436
8437
void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8438
{
8439
ImSwap(Size, r.Size);
8440
ImSwap(_SelectionOrder, r._SelectionOrder);
8441
_Storage.Data.swap(r._Storage.Data);
8442
}
8443
8444
bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8445
{
8446
return _Storage.GetInt(id, 0) != 0;
8447
}
8448
8449
static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8450
{
8451
int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8452
int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8453
return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8454
}
8455
8456
// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8457
// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8458
bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8459
{
8460
ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8461
ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8462
if (PreserveOrder && it == NULL && it_end != NULL)
8463
ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8464
if (it == NULL)
8465
it = _Storage.Data.Data;
8466
IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8467
if (it != it_end)
8468
while (it->val_i == 0 && it < it_end)
8469
it++;
8470
const bool has_more = (it != it_end);
8471
*opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8472
*out_id = has_more ? it->key : 0;
8473
if (PreserveOrder && !has_more)
8474
_Storage.BuildSortByKey();
8475
return has_more;
8476
}
8477
8478
void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8479
{
8480
int* p_int = _Storage.GetIntRef(id, 0);
8481
if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8482
else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8483
}
8484
8485
// Optimized for batch edits (with same value of 'selected')
8486
static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8487
{
8488
ImGuiStorage* storage = &selection->_Storage;
8489
ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8490
const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8491
if (selected == (is_contained && it->val_i != 0))
8492
return;
8493
if (selected && !is_contained)
8494
storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8495
else if (is_contained)
8496
it->val_i = selected ? selection_order : 0; // Modify in-place.
8497
selection->Size += selected ? +1 : -1;
8498
}
8499
8500
static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8501
{
8502
ImGuiStorage* storage = &selection->_Storage;
8503
if (selected && selection->Size != size_before_amends)
8504
storage->BuildSortByKey(); // When done selecting: sort everything
8505
}
8506
8507
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8508
// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8509
// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8510
// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8511
// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8512
// a lookup in order to have some way to iterate/interpolate between two items.
8513
// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8514
// and constructing a view index <> object id/ptr data structure anyway.
8515
// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8516
// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8517
// The most simple implementation (using indices everywhere) would look like:
8518
// for (ImGuiSelectionRequest& req : ms_io->Requests)
8519
// {
8520
// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8521
// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8522
// }
8523
void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8524
{
8525
// For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8526
// It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8527
// Other scheme may handle SetAll differently.
8528
IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8529
IM_ASSERT(AdapterIndexToStorageId != NULL);
8530
8531
// This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8532
// - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8533
// - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8534
// - 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.
8535
// - 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
8536
// left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.
8537
// FIXME-OPT: For each block of consecutive SetRange request:
8538
// - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8539
// - rewrite sorted storage a single time.
8540
for (ImGuiSelectionRequest& req : ms_io->Requests)
8541
{
8542
if (req.Type == ImGuiSelectionRequestType_SetAll)
8543
{
8544
Clear();
8545
if (req.Selected)
8546
{
8547
_Storage.Data.reserve(ms_io->ItemsCount);
8548
const int size_before_amends = _Storage.Data.Size;
8549
for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8550
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8551
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8552
}
8553
}
8554
else if (req.Type == ImGuiSelectionRequestType_SetRange)
8555
{
8556
const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8557
//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);
8558
if (selection_changes == 1 || (selection_changes < Size / 100))
8559
{
8560
// Multiple sorted insertion + copy likely to be faster.
8561
// Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8562
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8563
SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8564
}
8565
else
8566
{
8567
// Append insertion + single sort likely be faster.
8568
// Use req.RangeDirection to set order field so that Shift+Clicking from 1 to 5 is different than Shift+Clicking from 5 to 1
8569
const int size_before_amends = _Storage.Data.Size;
8570
int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8571
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8572
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8573
if (req.Selected)
8574
_SelectionOrder += selection_changes;
8575
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8576
}
8577
}
8578
}
8579
}
8580
8581
//-------------------------------------------------------------------------
8582
8583
ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8584
{
8585
UserData = NULL;
8586
AdapterSetItemSelected = NULL;
8587
}
8588
8589
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8590
// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8591
// This makes no assumption about underlying storage.
8592
void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8593
{
8594
IM_ASSERT(AdapterSetItemSelected);
8595
for (ImGuiSelectionRequest& req : ms_io->Requests)
8596
{
8597
if (req.Type == ImGuiSelectionRequestType_SetAll)
8598
for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8599
AdapterSetItemSelected(this, idx, req.Selected);
8600
if (req.Type == ImGuiSelectionRequestType_SetRange)
8601
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8602
AdapterSetItemSelected(this, idx, req.Selected);
8603
}
8604
}
8605
8606
//-------------------------------------------------------------------------
8607
// [SECTION] Widgets: ListBox
8608
//-------------------------------------------------------------------------
8609
// - BeginListBox()
8610
// - EndListBox()
8611
// - ListBox()
8612
//-------------------------------------------------------------------------
8613
8614
// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8615
// This handle some subtleties with capturing info from the label.
8616
// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle.
8617
// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8618
// 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).
8619
bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8620
{
8621
ImGuiContext& g = *GImGui;
8622
ImGuiWindow* window = GetCurrentWindow();
8623
if (window->SkipItems)
8624
return false;
8625
8626
const ImGuiStyle& style = g.Style;
8627
const ImGuiID id = GetID(label);
8628
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8629
8630
// Size default to hold ~7.25 items.
8631
// Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8632
ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8633
ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8634
ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8635
ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8636
g.NextItemData.ClearFlags();
8637
8638
if (!IsRectVisible(bb.Min, bb.Max))
8639
{
8640
ItemSize(bb.GetSize(), style.FramePadding.y);
8641
ItemAdd(bb, 0, &frame_bb);
8642
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8643
return false;
8644
}
8645
8646
// FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8647
BeginGroup();
8648
if (label_size.x > 0.0f)
8649
{
8650
ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8651
RenderText(label_pos, label);
8652
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8653
AlignTextToFramePadding();
8654
}
8655
8656
BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8657
return true;
8658
}
8659
8660
void ImGui::EndListBox()
8661
{
8662
ImGuiContext& g = *GImGui;
8663
ImGuiWindow* window = g.CurrentWindow;
8664
IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8665
IM_UNUSED(window);
8666
8667
EndChild();
8668
EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8669
}
8670
8671
bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8672
{
8673
const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8674
return value_changed;
8675
}
8676
8677
// This is merely a helper around BeginListBox(), EndListBox().
8678
// Considering using those directly to submit custom data or store selection differently.
8679
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)
8680
{
8681
ImGuiContext& g = *GImGui;
8682
8683
// Calculate size from "height_in_items"
8684
if (height_in_items < 0)
8685
height_in_items = ImMin(items_count, 7);
8686
float height_in_items_f = height_in_items + 0.25f;
8687
ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8688
8689
if (!BeginListBox(label, size))
8690
return false;
8691
8692
// Assume all items have even height (= 1 line of text). If you need items of different height,
8693
// you can create a custom version of ListBox() in your code without using the clipper.
8694
bool value_changed = false;
8695
ImGuiListClipper clipper;
8696
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.
8697
clipper.IncludeItemByIndex(*current_item);
8698
while (clipper.Step())
8699
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8700
{
8701
const char* item_text = getter(user_data, i);
8702
if (item_text == NULL)
8703
item_text = "*Unknown item*";
8704
8705
PushID(i);
8706
const bool item_selected = (i == *current_item);
8707
if (Selectable(item_text, item_selected))
8708
{
8709
*current_item = i;
8710
value_changed = true;
8711
}
8712
if (item_selected)
8713
SetItemDefaultFocus();
8714
PopID();
8715
}
8716
EndListBox();
8717
8718
if (value_changed)
8719
MarkItemEdited(g.LastItemData.ID);
8720
8721
return value_changed;
8722
}
8723
8724
//-------------------------------------------------------------------------
8725
// [SECTION] Widgets: PlotLines, PlotHistogram
8726
//-------------------------------------------------------------------------
8727
// - PlotEx() [Internal]
8728
// - PlotLines()
8729
// - PlotHistogram()
8730
//-------------------------------------------------------------------------
8731
// Plot/Graph widgets are not very good.
8732
// Consider writing your own, or using a third-party one, see:
8733
// - ImPlot https://github.com/epezent/implot
8734
// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8735
//-------------------------------------------------------------------------
8736
8737
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)
8738
{
8739
ImGuiContext& g = *GImGui;
8740
ImGuiWindow* window = GetCurrentWindow();
8741
if (window->SkipItems)
8742
return -1;
8743
8744
const ImGuiStyle& style = g.Style;
8745
const ImGuiID id = window->GetID(label);
8746
8747
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8748
const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8749
8750
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8751
const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8752
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));
8753
ItemSize(total_bb, style.FramePadding.y);
8754
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8755
return -1;
8756
bool hovered;
8757
ButtonBehavior(frame_bb, id, &hovered, NULL);
8758
8759
// Determine scale from values if not specified
8760
if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8761
{
8762
float v_min = FLT_MAX;
8763
float v_max = -FLT_MAX;
8764
for (int i = 0; i < values_count; i++)
8765
{
8766
const float v = values_getter(data, i);
8767
if (v != v) // Ignore NaN values
8768
continue;
8769
v_min = ImMin(v_min, v);
8770
v_max = ImMax(v_max, v);
8771
}
8772
if (scale_min == FLT_MAX)
8773
scale_min = v_min;
8774
if (scale_max == FLT_MAX)
8775
scale_max = v_max;
8776
}
8777
8778
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8779
8780
const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8781
int idx_hovered = -1;
8782
if (values_count >= values_count_min)
8783
{
8784
int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8785
int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8786
8787
// Tooltip on hover
8788
if (hovered && inner_bb.Contains(g.IO.MousePos))
8789
{
8790
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
8791
const int v_idx = (int)(t * item_count);
8792
IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8793
8794
const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8795
const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8796
if (plot_type == ImGuiPlotType_Lines)
8797
SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8798
else if (plot_type == ImGuiPlotType_Histogram)
8799
SetTooltip("%d: %8.4g", v_idx, v0);
8800
idx_hovered = v_idx;
8801
}
8802
8803
const float t_step = 1.0f / (float)res_w;
8804
const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8805
8806
float v0 = values_getter(data, (0 + values_offset) % values_count);
8807
float t0 = 0.0f;
8808
ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8809
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
8810
8811
const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8812
const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8813
8814
for (int n = 0; n < res_w; n++)
8815
{
8816
const float t1 = t0 + t_step;
8817
const int v1_idx = (int)(t0 * item_count + 0.5f);
8818
IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8819
const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8820
const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
8821
8822
// 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.
8823
ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
8824
ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8825
if (plot_type == ImGuiPlotType_Lines)
8826
{
8827
window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8828
}
8829
else if (plot_type == ImGuiPlotType_Histogram)
8830
{
8831
if (pos1.x >= pos0.x + 2.0f)
8832
pos1.x -= 1.0f;
8833
window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8834
}
8835
8836
t0 = t1;
8837
tp0 = tp1;
8838
}
8839
}
8840
8841
// Text overlay
8842
if (overlay_text)
8843
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));
8844
8845
if (label_size.x > 0.0f)
8846
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label);
8847
8848
// Return hovered index or -1 if none are hovered.
8849
// 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().
8850
return idx_hovered;
8851
}
8852
8853
struct ImGuiPlotArrayGetterData
8854
{
8855
const float* Values;
8856
int Stride;
8857
8858
ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8859
};
8860
8861
static float Plot_ArrayGetter(void* data, int idx)
8862
{
8863
ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8864
const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8865
return v;
8866
}
8867
8868
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)
8869
{
8870
ImGuiPlotArrayGetterData data(values, stride);
8871
PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8872
}
8873
8874
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)
8875
{
8876
PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8877
}
8878
8879
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)
8880
{
8881
ImGuiPlotArrayGetterData data(values, stride);
8882
PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8883
}
8884
8885
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)
8886
{
8887
PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8888
}
8889
8890
//-------------------------------------------------------------------------
8891
// [SECTION] Widgets: Value helpers
8892
// Those is not very useful, legacy API.
8893
//-------------------------------------------------------------------------
8894
// - Value()
8895
//-------------------------------------------------------------------------
8896
8897
void ImGui::Value(const char* prefix, bool b)
8898
{
8899
Text("%s: %s", prefix, (b ? "true" : "false"));
8900
}
8901
8902
void ImGui::Value(const char* prefix, int v)
8903
{
8904
Text("%s: %d", prefix, v);
8905
}
8906
8907
void ImGui::Value(const char* prefix, unsigned int v)
8908
{
8909
Text("%s: %d", prefix, v);
8910
}
8911
8912
void ImGui::Value(const char* prefix, float v, const char* float_format)
8913
{
8914
if (float_format)
8915
{
8916
char fmt[64];
8917
ImFormatString(fmt, IM_COUNTOF(fmt), "%%s: %s", float_format);
8918
Text(fmt, prefix, v);
8919
}
8920
else
8921
{
8922
Text("%s: %.3f", prefix, v);
8923
}
8924
}
8925
8926
//-------------------------------------------------------------------------
8927
// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8928
//-------------------------------------------------------------------------
8929
// - ImGuiMenuColumns [Internal]
8930
// - BeginMenuBar()
8931
// - EndMenuBar()
8932
// - BeginMainMenuBar()
8933
// - EndMainMenuBar()
8934
// - BeginMenu()
8935
// - EndMenu()
8936
// - MenuItemEx() [Internal]
8937
// - MenuItem()
8938
//-------------------------------------------------------------------------
8939
8940
// Helpers for internal use
8941
void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8942
{
8943
if (window_reappearing)
8944
memset(Widths, 0, sizeof(Widths));
8945
Spacing = (ImU16)spacing;
8946
CalcNextTotalWidth(true);
8947
memset(Widths, 0, sizeof(Widths));
8948
TotalWidth = NextTotalWidth;
8949
NextTotalWidth = 0;
8950
}
8951
8952
void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8953
{
8954
ImU16 offset = 0;
8955
bool want_spacing = false;
8956
for (int i = 0; i < IM_COUNTOF(Widths); i++)
8957
{
8958
ImU16 width = Widths[i];
8959
if (want_spacing && width > 0)
8960
offset += Spacing;
8961
want_spacing |= (width > 0);
8962
if (update_offsets)
8963
{
8964
if (i == 1) { OffsetLabel = offset; }
8965
if (i == 2) { OffsetShortcut = offset; }
8966
if (i == 3) { OffsetMark = offset; }
8967
}
8968
offset += width;
8969
}
8970
NextTotalWidth = offset;
8971
}
8972
8973
float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8974
{
8975
Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
8976
Widths[1] = ImMax(Widths[1], (ImU16)w_label);
8977
Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
8978
Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
8979
CalcNextTotalWidth(false);
8980
return (float)ImMax(TotalWidth, NextTotalWidth);
8981
}
8982
8983
// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8984
// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8985
// 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.
8986
// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8987
bool ImGui::BeginMenuBar()
8988
{
8989
ImGuiWindow* window = GetCurrentWindow();
8990
if (window->SkipItems)
8991
return false;
8992
if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8993
return false;
8994
8995
IM_ASSERT(!window->DC.MenuBarAppending);
8996
BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8997
PushID("##MenuBar");
8998
8999
// 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.
9000
// 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.
9001
const float border_top = ImMax(window->WindowBorderSize * 0.5f - window->TitleBarHeight, 0.0f);
9002
ImRect bar_rect = window->MenuBarRect();
9003
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));
9004
clip_rect.ClipWith(window->OuterRectClipped);
9005
PushClipRect(clip_rect.Min, clip_rect.Max, false);
9006
9007
// 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).
9008
window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
9009
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
9010
window->DC.IsSameLine = false;
9011
window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
9012
window->DC.MenuBarAppending = true;
9013
AlignTextToFramePadding();
9014
return true;
9015
}
9016
9017
void ImGui::EndMenuBar()
9018
{
9019
ImGuiWindow* window = GetCurrentWindow();
9020
if (window->SkipItems)
9021
return;
9022
ImGuiContext& g = *GImGui;
9023
9024
IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9025
IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
9026
IM_ASSERT(window->DC.MenuBarAppending);
9027
9028
// Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
9029
if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
9030
{
9031
// Try to find out if the request is for one of our child menu
9032
ImGuiWindow* nav_earliest_child = g.NavWindow;
9033
while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
9034
nav_earliest_child = nav_earliest_child->ParentWindow;
9035
if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
9036
{
9037
// To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
9038
// 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)
9039
const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
9040
IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
9041
FocusWindow(window);
9042
SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
9043
// FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
9044
if (g.NavCursorVisible)
9045
{
9046
g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
9047
g.NavCursorHideFrames = 2;
9048
}
9049
g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
9050
NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
9051
}
9052
}
9053
9054
PopClipRect();
9055
PopID();
9056
IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
9057
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.
9058
9059
// FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
9060
ImGuiGroupData& group_data = g.GroupStack.back();
9061
group_data.EmitItem = false;
9062
ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
9063
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
9064
EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
9065
window->DC.LayoutType = ImGuiLayoutType_Vertical;
9066
window->DC.IsSameLine = false;
9067
window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
9068
window->DC.MenuBarAppending = false;
9069
window->DC.CursorMaxPos = restore_cursor_max_pos;
9070
}
9071
9072
// Important: calling order matters!
9073
// FIXME: Somehow overlapping with docking tech.
9074
// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
9075
bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
9076
{
9077
IM_ASSERT(dir != ImGuiDir_None);
9078
9079
ImGuiWindow* bar_window = FindWindowByName(name);
9080
if (bar_window == NULL || bar_window->BeginCount == 0)
9081
{
9082
// Calculate and set window size/position
9083
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
9084
ImRect avail_rect = viewport->GetBuildWorkRect();
9085
ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
9086
ImVec2 pos = avail_rect.Min;
9087
if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
9088
pos[axis] = avail_rect.Max[axis] - axis_size;
9089
ImVec2 size = avail_rect.GetSize();
9090
size[axis] = axis_size;
9091
SetNextWindowPos(pos);
9092
SetNextWindowSize(size);
9093
9094
// Report our size into work area (for next frame) using actual window size
9095
if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
9096
viewport->BuildWorkInsetMin[axis] += axis_size;
9097
else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
9098
viewport->BuildWorkInsetMax[axis] += axis_size;
9099
}
9100
9101
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
9102
PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
9103
PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
9104
bool is_open = Begin(name, NULL, window_flags);
9105
PopStyleVar(2);
9106
9107
return is_open;
9108
}
9109
9110
bool ImGui::BeginMainMenuBar()
9111
{
9112
ImGuiContext& g = *GImGui;
9113
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
9114
9115
// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
9116
// FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
9117
// FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
9118
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
9119
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
9120
float height = GetFrameHeight();
9121
bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
9122
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
9123
if (!is_open)
9124
{
9125
End();
9126
return false;
9127
}
9128
9129
// Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356)
9130
g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings;
9131
BeginMenuBar();
9132
return is_open;
9133
}
9134
9135
void ImGui::EndMainMenuBar()
9136
{
9137
ImGuiContext& g = *GImGui;
9138
if (!g.CurrentWindow->DC.MenuBarAppending)
9139
{
9140
IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar
9141
return;
9142
}
9143
9144
EndMenuBar();
9145
g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356)
9146
9147
// When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
9148
// FIXME: With this strategy we won't be able to restore a NULL focus.
9149
if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0)
9150
FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
9151
9152
End();
9153
}
9154
9155
static bool IsRootOfOpenMenuSet()
9156
{
9157
ImGuiContext& g = *GImGui;
9158
ImGuiWindow* window = g.CurrentWindow;
9159
if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
9160
return false;
9161
9162
// Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
9163
// (e.g. inside menu bar vs loose menu items) based on parent ID.
9164
// This would however prevent the use of e.g. PushID() user code submitting menus.
9165
// Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
9166
// making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
9167
// Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
9168
// doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
9169
// 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.
9170
// 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
9171
// 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
9172
// it likely won't be a problem anyone runs into.
9173
const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
9174
if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
9175
return false;
9176
return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true);
9177
}
9178
9179
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
9180
{
9181
ImGuiWindow* window = GetCurrentWindow();
9182
if (window->SkipItems)
9183
return false;
9184
9185
ImGuiContext& g = *GImGui;
9186
const ImGuiStyle& style = g.Style;
9187
const ImGuiID id = window->GetID(label);
9188
bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
9189
9190
// 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)
9191
// 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.
9192
ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
9193
if (window->Flags & ImGuiWindowFlags_ChildMenu)
9194
window_flags |= ImGuiWindowFlags_ChildWindow;
9195
9196
// If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
9197
// 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.
9198
// If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
9199
if (g.MenusIdSubmittedThisFrame.contains(id))
9200
{
9201
if (menu_is_open)
9202
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)
9203
else
9204
g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
9205
return menu_is_open;
9206
}
9207
9208
// Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
9209
g.MenusIdSubmittedThisFrame.push_back(id);
9210
9211
ImVec2 label_size = CalcTextSize(label, NULL, true);
9212
9213
// 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)
9214
// This is only done for items for the menu set and not the full parent window.
9215
const bool menuset_is_open = IsRootOfOpenMenuSet();
9216
if (menuset_is_open)
9217
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9218
9219
// The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
9220
// However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
9221
// e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
9222
ImVec2 popup_pos;
9223
ImVec2 pos = window->DC.CursorPos;
9224
PushID(label);
9225
if (!enabled)
9226
BeginDisabled();
9227
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9228
bool pressed;
9229
9230
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9231
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
9232
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9233
{
9234
// Menu inside a horizontal menu bar
9235
// Selectable extend their highlight by half ItemSpacing in each direction.
9236
// For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
9237
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9238
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9239
float w = label_size.x;
9240
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, pos.y + window->DC.CurrLineTextBaseOffset);
9241
pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));
9242
LogSetNextTextDecoration("[", "]");
9243
RenderText(text_pos, label);
9244
PopStyleVar();
9245
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().
9246
popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), text_pos.y - style.FramePadding.y + window->MenuBarHeight);
9247
}
9248
else
9249
{
9250
// Menu inside a regular/vertical menu
9251
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9252
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9253
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9254
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9255
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
9256
float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9257
ImVec2 text_pos(window->DC.CursorPos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9258
pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9259
LogSetNextTextDecoration("", ">");
9260
RenderText(ImVec2(text_pos.x + offsets->OffsetLabel, text_pos.y), label);
9261
if (icon_w > 0.0f)
9262
RenderText(ImVec2(text_pos.x + offsets->OffsetIcon, text_pos.y), icon);
9263
RenderArrow(window->DrawList, ImVec2(text_pos.x + offsets->OffsetMark + extra_w + g.FontSize * 0.30f, text_pos.y), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
9264
popup_pos = ImVec2(pos.x, text_pos.y - style.WindowPadding.y);
9265
}
9266
if (!enabled)
9267
EndDisabled();
9268
9269
const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
9270
if (menuset_is_open)
9271
PopItemFlag();
9272
9273
bool want_open = false;
9274
bool want_open_nav_init = false;
9275
bool want_close = false;
9276
if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
9277
{
9278
// Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
9279
// Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
9280
bool moving_toward_child_menu = false;
9281
ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
9282
ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
9283
if (g.HoveredWindow == window && child_menu_window != NULL)
9284
{
9285
const float ref_unit = g.FontSize; // FIXME-DPI
9286
const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
9287
const ImRect next_window_rect = child_menu_window->Rect();
9288
ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
9289
ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
9290
ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
9291
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.
9292
ta.x += child_dir * -0.5f;
9293
tb.x += child_dir * ref_unit;
9294
tc.x += child_dir * ref_unit;
9295
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
9296
tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
9297
moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
9298
//GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
9299
}
9300
9301
// 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)
9302
// 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.
9303
// (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.)
9304
if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
9305
want_close = true;
9306
9307
// Open
9308
// (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
9309
if (!menu_is_open && pressed) // Click/activate to open
9310
want_open = true;
9311
else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
9312
want_open = true;
9313
else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
9314
want_open = true;
9315
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
9316
{
9317
want_open = want_open_nav_init = true;
9318
NavMoveRequestCancel();
9319
SetNavCursorVisibleAfterMove();
9320
}
9321
}
9322
else
9323
{
9324
// Menu bar
9325
if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
9326
{
9327
want_close = true;
9328
want_open = menu_is_open = false;
9329
}
9330
else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
9331
{
9332
want_open = true;
9333
}
9334
else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
9335
{
9336
want_open = true;
9337
NavMoveRequestCancel();
9338
}
9339
}
9340
9341
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.. }'
9342
want_close = true;
9343
if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
9344
ClosePopupToLevel(g.BeginPopupStack.Size, true);
9345
9346
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
9347
PopID();
9348
9349
if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
9350
{
9351
// 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.
9352
OpenPopup(label);
9353
}
9354
else if (want_open)
9355
{
9356
menu_is_open = true;
9357
OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
9358
}
9359
9360
if (menu_is_open)
9361
{
9362
ImGuiLastItemData last_item_in_parent = g.LastItemData;
9363
SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
9364
PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
9365
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)
9366
PopStyleVar();
9367
if (menu_is_open)
9368
{
9369
// Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
9370
// Perform an init request in the case the popup was already open (via a previous mouse hover)
9371
if (want_open && want_open_nav_init && !g.NavInitRequest)
9372
{
9373
FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
9374
NavInitWindow(g.CurrentWindow, false);
9375
}
9376
9377
// Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
9378
// (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
9379
g.LastItemData = last_item_in_parent;
9380
if (g.HoveredWindow == window)
9381
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
9382
}
9383
}
9384
else
9385
{
9386
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
9387
}
9388
9389
return menu_is_open;
9390
}
9391
9392
bool ImGui::BeginMenu(const char* label, bool enabled)
9393
{
9394
return BeginMenuEx(label, NULL, enabled);
9395
}
9396
9397
void ImGui::EndMenu()
9398
{
9399
// Nav: When a left move request our menu failed, close ourselves.
9400
ImGuiContext& g = *GImGui;
9401
ImGuiWindow* window = g.CurrentWindow;
9402
IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
9403
ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
9404
if (window->BeginCount == window->BeginCountPreviousFrame)
9405
if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9406
if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9407
{
9408
ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9409
NavMoveRequestCancel();
9410
}
9411
9412
EndPopup();
9413
}
9414
9415
bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9416
{
9417
ImGuiWindow* window = GetCurrentWindow();
9418
if (window->SkipItems)
9419
return false;
9420
9421
ImGuiContext& g = *GImGui;
9422
ImGuiStyle& style = g.Style;
9423
ImVec2 pos = window->DC.CursorPos;
9424
ImVec2 label_size = CalcTextSize(label, NULL, true);
9425
9426
// See BeginMenuEx() for comments about this.
9427
const bool menuset_is_open = IsRootOfOpenMenuSet();
9428
if (menuset_is_open)
9429
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9430
9431
// We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9432
// 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.
9433
bool pressed;
9434
PushID(label);
9435
if (!enabled)
9436
BeginDisabled();
9437
9438
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9439
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9440
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9441
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9442
{
9443
// Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9444
// Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9445
float w = label_size.x;
9446
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9447
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9448
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9449
pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f));
9450
PopStyleVar();
9451
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9452
RenderText(text_pos, label);
9453
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().
9454
}
9455
else
9456
{
9457
// Menu item inside a vertical menu
9458
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9459
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.)
9460
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9461
float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9462
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9463
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9464
float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9465
ImVec2 text_pos(pos.x, pos.y + window->DC.CurrLineTextBaseOffset);
9466
pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9467
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9468
{
9469
RenderText(text_pos + ImVec2(offsets->OffsetLabel, 0.0f), label);
9470
if (icon_w > 0.0f)
9471
RenderText(text_pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9472
if (shortcut_w > 0.0f)
9473
{
9474
PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9475
LogSetNextTextDecoration("(", ")");
9476
RenderText(text_pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9477
PopStyleColor();
9478
}
9479
if (selected)
9480
RenderCheckMark(window->DrawList, text_pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9481
}
9482
}
9483
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9484
if (!enabled)
9485
EndDisabled();
9486
PopID();
9487
if (menuset_is_open)
9488
PopItemFlag();
9489
9490
return pressed;
9491
}
9492
9493
bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9494
{
9495
return MenuItemEx(label, NULL, shortcut, selected, enabled);
9496
}
9497
9498
bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9499
{
9500
if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9501
{
9502
if (p_selected)
9503
*p_selected = !*p_selected;
9504
return true;
9505
}
9506
return false;
9507
}
9508
9509
//-------------------------------------------------------------------------
9510
// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9511
//-------------------------------------------------------------------------
9512
// - BeginTabBar()
9513
// - BeginTabBarEx() [Internal]
9514
// - EndTabBar()
9515
// - TabBarLayout() [Internal]
9516
// - TabBarCalcTabID() [Internal]
9517
// - TabBarCalcMaxTabWidth() [Internal]
9518
// - TabBarFindTabById() [Internal]
9519
// - TabBarFindTabByOrder() [Internal]
9520
// - TabBarGetCurrentTab() [Internal]
9521
// - TabBarGetTabName() [Internal]
9522
// - TabBarRemoveTab() [Internal]
9523
// - TabBarCloseTab() [Internal]
9524
// - TabBarScrollClamp() [Internal]
9525
// - TabBarScrollToTab() [Internal]
9526
// - TabBarQueueFocus() [Internal]
9527
// - TabBarQueueReorder() [Internal]
9528
// - TabBarProcessReorderFromMousePos() [Internal]
9529
// - TabBarProcessReorder() [Internal]
9530
// - TabBarScrollingButtons() [Internal]
9531
// - TabBarTabListPopupButton() [Internal]
9532
//-------------------------------------------------------------------------
9533
9534
struct ImGuiTabBarSection
9535
{
9536
int TabCount; // Number of tabs in this section.
9537
float Width; // Sum of width of tabs in this section (after shrinking down)
9538
float WidthAfterShrinkMinWidth;
9539
float Spacing; // Horizontal spacing at the end of the section.
9540
9541
ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
9542
};
9543
9544
namespace ImGui
9545
{
9546
static void TabBarLayout(ImGuiTabBar* tab_bar);
9547
static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9548
static float TabBarCalcMaxTabWidth();
9549
static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9550
static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9551
static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9552
static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9553
}
9554
9555
ImGuiTabBar::ImGuiTabBar()
9556
{
9557
memset(this, 0, sizeof(*this));
9558
CurrFrameVisible = PrevFrameVisible = -1;
9559
LastTabItemIdx = -1;
9560
}
9561
9562
static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9563
{
9564
return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9565
}
9566
9567
static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9568
{
9569
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9570
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9571
const int a_section = TabItemGetSectionIdx(a);
9572
const int b_section = TabItemGetSectionIdx(b);
9573
if (a_section != b_section)
9574
return a_section - b_section;
9575
return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9576
}
9577
9578
static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9579
{
9580
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9581
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9582
return (int)(a->BeginOrder - b->BeginOrder);
9583
}
9584
9585
static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9586
{
9587
ImGuiContext& g = *GImGui;
9588
return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9589
}
9590
9591
static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9592
{
9593
ImGuiContext& g = *GImGui;
9594
if (g.TabBars.Contains(tab_bar))
9595
return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9596
return ImGuiPtrOrIndex(tab_bar);
9597
}
9598
9599
ImGuiTabBar* ImGui::TabBarFindByID(ImGuiID id)
9600
{
9601
ImGuiContext& g = *GImGui;
9602
return g.TabBars.GetByKey(id);
9603
}
9604
9605
// Remove TabBar data (currently only used by TestEngine)
9606
void ImGui::TabBarRemove(ImGuiTabBar* tab_bar)
9607
{
9608
ImGuiContext& g = *GImGui;
9609
g.TabBars.Remove(tab_bar->ID, tab_bar);
9610
}
9611
9612
bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9613
{
9614
ImGuiContext& g = *GImGui;
9615
ImGuiWindow* window = g.CurrentWindow;
9616
if (window->SkipItems)
9617
return false;
9618
9619
ImGuiID id = window->GetID(str_id);
9620
ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9621
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);
9622
tab_bar->ID = id;
9623
tab_bar->SeparatorMinX = tab_bar_bb.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9624
tab_bar->SeparatorMaxX = tab_bar_bb.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9625
//if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9626
flags |= ImGuiTabBarFlags_IsFocused;
9627
return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9628
}
9629
9630
bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9631
{
9632
ImGuiContext& g = *GImGui;
9633
ImGuiWindow* window = g.CurrentWindow;
9634
if (window->SkipItems)
9635
return false;
9636
9637
IM_ASSERT(tab_bar->ID != 0);
9638
if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9639
PushOverrideID(tab_bar->ID);
9640
9641
// Add to stack
9642
g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9643
g.CurrentTabBar = tab_bar;
9644
tab_bar->Window = window;
9645
9646
// Append with multiple BeginTabBar()/EndTabBar() pairs.
9647
tab_bar->BackupCursorPos = window->DC.CursorPos;
9648
if (tab_bar->CurrFrameVisible == g.FrameCount)
9649
{
9650
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9651
tab_bar->BeginCount++;
9652
return true;
9653
}
9654
9655
// Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9656
if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9657
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9658
tab_bar->TabsAddedNew = false;
9659
9660
// Flags
9661
if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9662
flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9663
9664
tab_bar->Flags = flags;
9665
tab_bar->BarRect = tab_bar_bb;
9666
tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9667
tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9668
tab_bar->CurrFrameVisible = g.FrameCount;
9669
tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9670
tab_bar->CurrTabsContentsHeight = 0.0f;
9671
tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9672
tab_bar->FramePadding = g.Style.FramePadding;
9673
tab_bar->TabsActiveCount = 0;
9674
tab_bar->LastTabItemIdx = -1;
9675
tab_bar->BeginCount = 1;
9676
9677
// Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9678
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9679
9680
// Draw separator
9681
// (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9682
const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9683
if (g.Style.TabBarBorderSize > 0.0f)
9684
{
9685
const float y = tab_bar->BarRect.Max.y;
9686
window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9687
}
9688
return true;
9689
}
9690
9691
void ImGui::EndTabBar()
9692
{
9693
ImGuiContext& g = *GImGui;
9694
ImGuiWindow* window = g.CurrentWindow;
9695
if (window->SkipItems)
9696
return;
9697
9698
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9699
if (tab_bar == NULL)
9700
{
9701
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9702
return;
9703
}
9704
9705
// Fallback in case no TabItem have been submitted
9706
if (tab_bar->WantLayout)
9707
TabBarLayout(tab_bar);
9708
9709
// Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9710
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9711
if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9712
{
9713
tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9714
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9715
}
9716
else
9717
{
9718
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9719
}
9720
if (tab_bar->BeginCount > 1)
9721
window->DC.CursorPos = tab_bar->BackupCursorPos;
9722
9723
tab_bar->LastTabItemIdx = -1;
9724
if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9725
PopID();
9726
9727
g.CurrentTabBarStack.pop_back();
9728
g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9729
}
9730
9731
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9732
static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9733
{
9734
return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9735
}
9736
9737
// This is called only once a frame before by the first call to ItemTab()
9738
// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9739
static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9740
{
9741
ImGuiContext& g = *GImGui;
9742
tab_bar->WantLayout = false;
9743
9744
// Track selected tab when resizing our parent down
9745
const bool scroll_to_selected_tab = (tab_bar->BarRectPrevWidth > tab_bar->BarRect.GetWidth());
9746
tab_bar->BarRectPrevWidth = tab_bar->BarRect.GetWidth();
9747
9748
// Garbage collect by compacting list
9749
// Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9750
int tab_dst_n = 0;
9751
bool need_sort_by_section = false;
9752
ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9753
for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9754
{
9755
ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9756
if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9757
{
9758
// Remove tab
9759
if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9760
if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9761
if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9762
continue;
9763
}
9764
if (tab_dst_n != tab_src_n)
9765
tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9766
9767
tab = &tab_bar->Tabs[tab_dst_n];
9768
tab->IndexDuringLayout = (ImS16)tab_dst_n;
9769
9770
// We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9771
int curr_tab_section_n = TabItemGetSectionIdx(tab);
9772
if (tab_dst_n > 0)
9773
{
9774
ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9775
int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
9776
if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9777
need_sort_by_section = true;
9778
if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9779
need_sort_by_section = true;
9780
}
9781
9782
sections[curr_tab_section_n].TabCount++;
9783
tab_dst_n++;
9784
}
9785
if (tab_bar->Tabs.Size != tab_dst_n)
9786
tab_bar->Tabs.resize(tab_dst_n);
9787
9788
if (need_sort_by_section)
9789
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
9790
9791
// Calculate spacing between sections
9792
const float tab_spacing = g.Style.ItemInnerSpacing.x;
9793
sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? tab_spacing : 0.0f;
9794
sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? tab_spacing : 0.0f;
9795
9796
// Setup next selected tab
9797
ImGuiID scroll_to_tab_id = 0;
9798
if (tab_bar->NextSelectedTabId)
9799
{
9800
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9801
tab_bar->NextSelectedTabId = 0;
9802
scroll_to_tab_id = tab_bar->SelectedTabId;
9803
}
9804
9805
// Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9806
if (tab_bar->ReorderRequestTabId != 0)
9807
{
9808
if (TabBarProcessReorder(tab_bar))
9809
if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9810
scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9811
tab_bar->ReorderRequestTabId = 0;
9812
}
9813
9814
// Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9815
const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9816
if (tab_list_popup_button)
9817
if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9818
scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9819
9820
// Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9821
// (whereas our tabs are stored as: leading, central, trailing)
9822
int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9823
g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
9824
9825
// Minimum shrink width
9826
const float shrink_min_width = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed) ? g.Style.TabMinWidthShrink : 1.0f;
9827
9828
// Compute ideal tabs widths + store them into shrink buffer
9829
ImGuiTabItem* most_recently_selected_tab = NULL;
9830
int curr_section_n = -1;
9831
bool found_selected_tab_id = false;
9832
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9833
{
9834
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9835
IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9836
9837
if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9838
most_recently_selected_tab = tab;
9839
if (tab->ID == tab_bar->SelectedTabId)
9840
found_selected_tab_id = true;
9841
if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9842
scroll_to_tab_id = tab->ID;
9843
9844
// Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9845
// Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9846
// and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9847
const char* tab_name = TabBarGetTabName(tab_bar, tab);
9848
const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9849
tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
9850
if ((tab->Flags & ImGuiTabItemFlags_Button) == 0)
9851
tab->ContentWidth = ImMax(tab->ContentWidth, g.Style.TabMinWidthBase);
9852
9853
int section_n = TabItemGetSectionIdx(tab);
9854
ImGuiTabBarSection* section = &sections[section_n];
9855
section->Width += tab->ContentWidth + (section_n == curr_section_n ? tab_spacing : 0.0f);
9856
section->WidthAfterShrinkMinWidth += ImMin(tab->ContentWidth, shrink_min_width) + (section_n == curr_section_n ? tab_spacing : 0.0f);
9857
curr_section_n = section_n;
9858
9859
// Store data so we can build an array sorted by width if we need to shrink tabs down
9860
IM_MSVC_WARNING_SUPPRESS(6385);
9861
ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9862
shrink_width_item->Index = tab_n;
9863
shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9864
tab->Width = ImMax(tab->ContentWidth, 1.0f);
9865
}
9866
9867
// Compute total ideal width (used for e.g. auto-resizing a window)
9868
float width_all_tabs_after_min_width_shrink = 0.0f;
9869
tab_bar->WidthAllTabsIdeal = 0.0f;
9870
for (int section_n = 0; section_n < 3; section_n++)
9871
{
9872
tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9873
width_all_tabs_after_min_width_shrink += sections[section_n].WidthAfterShrinkMinWidth + sections[section_n].Spacing;
9874
}
9875
9876
// Horizontal scrolling buttons
9877
// Important: note that TabBarScrollButtons() will alter BarRect.Max.x.
9878
const bool can_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
9879
const float width_all_tabs_to_use_for_scroll = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) ? tab_bar->WidthAllTabs : width_all_tabs_after_min_width_shrink;
9880
tab_bar->ScrollButtonEnabled = ((width_all_tabs_to_use_for_scroll > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && can_scroll);
9881
if (tab_bar->ScrollButtonEnabled)
9882
if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9883
{
9884
scroll_to_tab_id = scroll_and_select_tab->ID;
9885
if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9886
tab_bar->SelectedTabId = scroll_to_tab_id;
9887
}
9888
if (scroll_to_tab_id == 0 && scroll_to_selected_tab)
9889
scroll_to_tab_id = tab_bar->SelectedTabId;
9890
9891
// Shrink widths if full tabs don't fit in their allocated space
9892
float section_0_w = sections[0].Width + sections[0].Spacing;
9893
float section_1_w = sections[1].Width + sections[1].Spacing;
9894
float section_2_w = sections[2].Width + sections[2].Spacing;
9895
bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9896
float width_excess;
9897
if (central_section_is_visible)
9898
width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
9899
else
9900
width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9901
9902
// With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9903
const bool can_shrink = (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyShrink) || (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyMixed);
9904
if (width_excess >= 1.0f && (can_shrink || !central_section_is_visible))
9905
{
9906
int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9907
int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9908
ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess, shrink_min_width);
9909
9910
// Apply shrunk values into tabs and sections
9911
for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9912
{
9913
ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9914
float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9915
if (shrinked_width < 0.0f)
9916
continue;
9917
9918
shrinked_width = ImMax(1.0f, shrinked_width);
9919
int section_n = TabItemGetSectionIdx(tab);
9920
sections[section_n].Width -= (tab->Width - shrinked_width);
9921
tab->Width = shrinked_width;
9922
}
9923
}
9924
9925
// Layout all active tabs
9926
int section_tab_index = 0;
9927
float tab_offset = 0.0f;
9928
tab_bar->WidthAllTabs = 0.0f;
9929
for (int section_n = 0; section_n < 3; section_n++)
9930
{
9931
ImGuiTabBarSection* section = &sections[section_n];
9932
if (section_n == 2)
9933
tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
9934
9935
for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9936
{
9937
ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9938
tab->Offset = tab_offset;
9939
tab->NameOffset = -1;
9940
tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9941
}
9942
tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
9943
tab_offset += section->Spacing;
9944
section_tab_index += section->TabCount;
9945
}
9946
9947
// Clear name buffers
9948
tab_bar->TabsNames.Buf.resize(0);
9949
9950
// If we have lost the selected tab, select the next most recently active one
9951
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9952
if (found_selected_tab_id == false && !tab_bar_appearing)
9953
tab_bar->SelectedTabId = 0;
9954
if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9955
scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9956
9957
// Lock in visible tab
9958
tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9959
tab_bar->VisibleTabWasSubmitted = false;
9960
9961
// Apply request requests
9962
if (scroll_to_tab_id != 0)
9963
TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
9964
else if (tab_bar->ScrollButtonEnabled && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
9965
{
9966
const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9967
const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9968
if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
9969
{
9970
const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9971
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9972
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
9973
}
9974
SetKeyOwner(wheel_key, tab_bar->ID);
9975
}
9976
9977
// Update scrolling
9978
tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
9979
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
9980
if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9981
{
9982
// Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9983
// Teleport if we are aiming far off the visible line
9984
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
9985
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9986
const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9987
tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9988
}
9989
else
9990
{
9991
tab_bar->ScrollingSpeed = 0.0f;
9992
}
9993
tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9994
tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9995
9996
// Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9997
ImGuiWindow* window = g.CurrentWindow;
9998
window->DC.CursorPos = tab_bar->BarRect.Min;
9999
ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
10000
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
10001
}
10002
10003
// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
10004
static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
10005
{
10006
IM_ASSERT(docked_window == NULL); // master branch only
10007
IM_UNUSED(docked_window);
10008
if (tab_bar->Flags & ImGuiTabBarFlags_DockNode)
10009
{
10010
ImGuiID id = ImHashStr(label);
10011
KeepAliveID(id);
10012
return id;
10013
}
10014
else
10015
{
10016
ImGuiWindow* window = GImGui->CurrentWindow;
10017
return window->GetID(label);
10018
}
10019
}
10020
10021
static float ImGui::TabBarCalcMaxTabWidth()
10022
{
10023
ImGuiContext& g = *GImGui;
10024
return g.FontSize * 20.0f;
10025
}
10026
10027
ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10028
{
10029
if (tab_id != 0)
10030
for (int n = 0; n < tab_bar->Tabs.Size; n++)
10031
if (tab_bar->Tabs[n].ID == tab_id)
10032
return &tab_bar->Tabs[n];
10033
return NULL;
10034
}
10035
10036
// Order = visible order, not submission order! (which is tab->BeginOrder)
10037
ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
10038
{
10039
if (order < 0 || order >= tab_bar->Tabs.Size)
10040
return NULL;
10041
return &tab_bar->Tabs[order];
10042
}
10043
10044
ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
10045
{
10046
if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
10047
return NULL;
10048
return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10049
}
10050
10051
const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10052
{
10053
if (tab->NameOffset == -1)
10054
return "N/A";
10055
IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
10056
return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
10057
}
10058
10059
// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
10060
void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
10061
{
10062
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10063
tab_bar->Tabs.erase(tab);
10064
if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
10065
if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
10066
if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
10067
}
10068
10069
// Called on manual closure attempt
10070
void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10071
{
10072
if (tab->Flags & ImGuiTabItemFlags_Button)
10073
return; // A button appended with TabItemButton().
10074
10075
if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
10076
{
10077
// This will remove a frame of lag for selecting another tab on closure.
10078
// 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
10079
tab->WantClose = true;
10080
if (tab_bar->VisibleTabId == tab->ID)
10081
{
10082
tab->LastFrameVisible = -1;
10083
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
10084
}
10085
}
10086
else
10087
{
10088
// Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
10089
if (tab_bar->VisibleTabId != tab->ID)
10090
TabBarQueueFocus(tab_bar, tab);
10091
}
10092
}
10093
10094
static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
10095
{
10096
scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
10097
return ImMax(scrolling, 0.0f);
10098
}
10099
10100
// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
10101
static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
10102
{
10103
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
10104
if (tab == NULL)
10105
return;
10106
if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
10107
return;
10108
10109
ImGuiContext& g = *GImGui;
10110
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)
10111
int order = TabBarGetTabOrder(tab_bar, tab);
10112
10113
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
10114
float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
10115
10116
// We make all tabs positions all relative Sections[0].Width to make code simpler
10117
float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
10118
float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
10119
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
10120
if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
10121
{
10122
// Scroll to the left
10123
tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
10124
tab_bar->ScrollingTarget = tab_x1;
10125
}
10126
else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
10127
{
10128
// Scroll to the right
10129
tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
10130
tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
10131
}
10132
}
10133
10134
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
10135
{
10136
tab_bar->NextSelectedTabId = tab->ID;
10137
}
10138
10139
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
10140
{
10141
IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
10142
ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
10143
tab_bar->NextSelectedTabId = tab_id;
10144
}
10145
10146
void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
10147
{
10148
IM_ASSERT(offset != 0);
10149
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10150
tab_bar->ReorderRequestTabId = tab->ID;
10151
tab_bar->ReorderRequestOffset = (ImS16)offset;
10152
}
10153
10154
void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
10155
{
10156
ImGuiContext& g = *GImGui;
10157
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
10158
if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
10159
return;
10160
10161
const float tab_spacing = g.Style.ItemInnerSpacing.x;
10162
const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10163
const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
10164
10165
// Count number of contiguous tabs we are crossing over
10166
const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
10167
const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
10168
int dst_idx = src_idx;
10169
for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
10170
{
10171
// Reordered tabs must share the same section
10172
const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
10173
if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
10174
break;
10175
if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
10176
break;
10177
dst_idx = i;
10178
10179
// Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
10180
const float x1 = bar_offset + dst_tab->Offset - tab_spacing;
10181
const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + tab_spacing;
10182
//GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
10183
if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
10184
break;
10185
}
10186
10187
if (dst_idx != src_idx)
10188
TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
10189
}
10190
10191
bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
10192
{
10193
ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
10194
if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
10195
return false;
10196
10197
//IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
10198
int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
10199
if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
10200
return false;
10201
10202
// Reordered tabs must share the same section
10203
// (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
10204
ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
10205
if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
10206
return false;
10207
if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
10208
return false;
10209
10210
ImGuiTabItem item_tmp = *tab1;
10211
ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
10212
ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
10213
const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
10214
memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
10215
*tab2 = item_tmp;
10216
10217
if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
10218
MarkIniSettingsDirty();
10219
return true;
10220
}
10221
10222
static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
10223
{
10224
ImGuiContext& g = *GImGui;
10225
ImGuiWindow* window = g.CurrentWindow;
10226
10227
const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
10228
const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
10229
10230
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10231
//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));
10232
10233
int select_dir = 0;
10234
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10235
arrow_col.w *= 0.5f;
10236
10237
PushStyleColor(ImGuiCol_Text, arrow_col);
10238
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10239
PushItemFlag(ImGuiItemFlags_ButtonRepeat | ImGuiItemFlags_NoNav, true);
10240
const float backup_repeat_delay = g.IO.KeyRepeatDelay;
10241
const float backup_repeat_rate = g.IO.KeyRepeatRate;
10242
g.IO.KeyRepeatDelay = 0.250f;
10243
g.IO.KeyRepeatRate = 0.200f;
10244
float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
10245
window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
10246
if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10247
select_dir = -1;
10248
window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
10249
if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
10250
select_dir = +1;
10251
PopItemFlag();
10252
PopStyleColor(2);
10253
g.IO.KeyRepeatRate = backup_repeat_rate;
10254
g.IO.KeyRepeatDelay = backup_repeat_delay;
10255
10256
ImGuiTabItem* tab_to_scroll_to = NULL;
10257
if (select_dir != 0)
10258
if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
10259
{
10260
int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
10261
int target_order = selected_order + select_dir;
10262
10263
// Skip tab item buttons until another tab item is found or end is reached
10264
while (tab_to_scroll_to == NULL)
10265
{
10266
// If we are at the end of the list, still scroll to make our tab visible
10267
tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
10268
10269
// Cross through buttons
10270
// (even if first/last item is a button, return it so we can update the scroll)
10271
if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
10272
{
10273
target_order += select_dir;
10274
selected_order += select_dir;
10275
tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
10276
}
10277
}
10278
}
10279
window->DC.CursorPos = backup_cursor_pos;
10280
tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
10281
10282
return tab_to_scroll_to;
10283
}
10284
10285
static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
10286
{
10287
ImGuiContext& g = *GImGui;
10288
ImGuiWindow* window = g.CurrentWindow;
10289
10290
// We use g.Style.FramePadding.y to match the square ArrowButton size
10291
const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
10292
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
10293
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
10294
tab_bar->BarRect.Min.x += tab_list_popup_button_width;
10295
10296
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
10297
arrow_col.w *= 0.5f;
10298
PushStyleColor(ImGuiCol_Text, arrow_col);
10299
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
10300
bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
10301
PopStyleColor(2);
10302
10303
ImGuiTabItem* tab_to_select = NULL;
10304
if (open)
10305
{
10306
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
10307
{
10308
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
10309
if (tab->Flags & ImGuiTabItemFlags_Button)
10310
continue;
10311
10312
const char* tab_name = TabBarGetTabName(tab_bar, tab);
10313
if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
10314
tab_to_select = tab;
10315
}
10316
EndCombo();
10317
}
10318
10319
window->DC.CursorPos = backup_cursor_pos;
10320
return tab_to_select;
10321
}
10322
10323
//-------------------------------------------------------------------------
10324
// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
10325
//-------------------------------------------------------------------------
10326
// - BeginTabItem()
10327
// - EndTabItem()
10328
// - TabItemButton()
10329
// - TabItemEx() [Internal]
10330
// - SetTabItemClosed()
10331
// - TabItemCalcSize() [Internal]
10332
// - TabItemBackground() [Internal]
10333
// - TabItemLabelAndCloseButton() [Internal]
10334
//-------------------------------------------------------------------------
10335
10336
bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
10337
{
10338
ImGuiContext& g = *GImGui;
10339
ImGuiWindow* window = g.CurrentWindow;
10340
if (window->SkipItems)
10341
return false;
10342
10343
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10344
if (tab_bar == NULL)
10345
{
10346
IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
10347
return false;
10348
}
10349
IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
10350
10351
bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
10352
if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
10353
{
10354
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10355
PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
10356
}
10357
return ret;
10358
}
10359
10360
void ImGui::EndTabItem()
10361
{
10362
ImGuiContext& g = *GImGui;
10363
ImGuiWindow* window = g.CurrentWindow;
10364
if (window->SkipItems)
10365
return;
10366
10367
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10368
if (tab_bar == NULL)
10369
{
10370
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10371
return;
10372
}
10373
IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
10374
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
10375
if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
10376
PopID();
10377
}
10378
10379
bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
10380
{
10381
ImGuiContext& g = *GImGui;
10382
ImGuiWindow* window = g.CurrentWindow;
10383
if (window->SkipItems)
10384
return false;
10385
10386
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10387
if (tab_bar == NULL)
10388
{
10389
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10390
return false;
10391
}
10392
return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
10393
}
10394
10395
void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width)
10396
{
10397
ImGuiContext& g = *GImGui;
10398
ImGuiWindow* window = g.CurrentWindow;
10399
if (window->SkipItems)
10400
return;
10401
10402
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10403
if (tab_bar == NULL)
10404
{
10405
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
10406
return;
10407
}
10408
SetNextItemWidth(width);
10409
TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL);
10410
}
10411
10412
bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
10413
{
10414
// Layout whole tab bar if not already done
10415
ImGuiContext& g = *GImGui;
10416
if (tab_bar->WantLayout)
10417
{
10418
ImGuiNextItemData backup_next_item_data = g.NextItemData;
10419
TabBarLayout(tab_bar);
10420
g.NextItemData = backup_next_item_data;
10421
}
10422
ImGuiWindow* window = g.CurrentWindow;
10423
if (window->SkipItems)
10424
return false;
10425
10426
const ImGuiStyle& style = g.Style;
10427
const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10428
10429
// If the user called us with *p_open == false, we early out and don't render.
10430
// 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.
10431
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10432
if (p_open && !*p_open)
10433
{
10434
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10435
return false;
10436
}
10437
10438
IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10439
IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10440
10441
// Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10442
if (flags & ImGuiTabItemFlags_NoCloseButton)
10443
p_open = NULL;
10444
else if (p_open == NULL)
10445
flags |= ImGuiTabItemFlags_NoCloseButton;
10446
10447
// Acquire tab data
10448
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10449
bool tab_is_new = false;
10450
if (tab == NULL)
10451
{
10452
tab_bar->Tabs.push_back(ImGuiTabItem());
10453
tab = &tab_bar->Tabs.back();
10454
tab->ID = id;
10455
tab_bar->TabsAddedNew = tab_is_new = true;
10456
}
10457
tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10458
10459
// Calculate tab contents size
10460
ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10461
tab->RequestedWidth = -1.0f;
10462
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10463
size.x = tab->RequestedWidth = g.NextItemData.Width;
10464
if (tab_is_new)
10465
tab->Width = ImMax(1.0f, size.x);
10466
tab->ContentWidth = size.x;
10467
tab->BeginOrder = tab_bar->TabsActiveCount++;
10468
10469
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10470
const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10471
const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10472
const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10473
const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10474
tab->LastFrameVisible = g.FrameCount;
10475
tab->Flags = flags;
10476
10477
// Append name _WITH_ the zero-terminator
10478
if (docked_window != NULL)
10479
{
10480
IM_ASSERT(docked_window == NULL); // master branch only
10481
}
10482
else
10483
{
10484
tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10485
tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1);
10486
}
10487
10488
// Update selected tab
10489
if (!is_tab_button)
10490
{
10491
if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10492
if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10493
TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10494
if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10495
TabBarQueueFocus(tab_bar, tab);
10496
}
10497
10498
// Lock visibility
10499
// (Note: tab_contents_visible != tab_selected... because Ctrl+Tab operations may preview some tabs without selecting them!)
10500
bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10501
if (tab_contents_visible)
10502
tab_bar->VisibleTabWasSubmitted = true;
10503
10504
// On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10505
if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing)
10506
if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10507
tab_contents_visible = true;
10508
10509
// Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10510
// and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10511
if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10512
{
10513
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10514
if (is_tab_button)
10515
return false;
10516
return tab_contents_visible;
10517
}
10518
10519
if (tab_bar->SelectedTabId == id)
10520
tab->LastFrameSelected = g.FrameCount;
10521
10522
// Backup current layout position
10523
const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10524
10525
// Layout
10526
const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10527
size.x = tab->Width;
10528
if (is_central_section)
10529
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10530
else
10531
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10532
ImVec2 pos = window->DC.CursorPos;
10533
ImRect bb(pos, pos + size);
10534
10535
// 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)
10536
const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10537
if (want_clip_rect)
10538
PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10539
10540
ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10541
ItemSize(bb.GetSize(), style.FramePadding.y);
10542
window->DC.CursorMaxPos = backup_cursor_max_pos;
10543
10544
if (!ItemAdd(bb, id))
10545
{
10546
if (want_clip_rect)
10547
PopClipRect();
10548
window->DC.CursorPos = backup_main_cursor_pos;
10549
return tab_contents_visible;
10550
}
10551
10552
// Click to Select a tab
10553
// Allow the close button to overlap
10554
ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10555
if (g.DragDropActive)
10556
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10557
bool hovered, held, pressed;
10558
if (flags & ImGuiTabItemFlags_Invisible)
10559
hovered = held = pressed = false;
10560
else
10561
pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10562
if (pressed && !is_tab_button)
10563
TabBarQueueFocus(tab_bar, tab);
10564
10565
// Drag and drop: re-order tabs
10566
if (held && !tab_appearing && IsMouseDragging(0))
10567
{
10568
if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable))
10569
{
10570
// While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10571
if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10572
{
10573
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10574
}
10575
else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10576
{
10577
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10578
}
10579
}
10580
}
10581
10582
#if 0
10583
if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10584
{
10585
// Enlarge tab display when hovering
10586
bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10587
display_draw_list = GetForegroundDrawList(window);
10588
TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10589
}
10590
#endif
10591
10592
// Render tab shape
10593
const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible);
10594
if (is_visible)
10595
{
10596
ImDrawList* display_draw_list = window->DrawList;
10597
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));
10598
TabItemBackground(display_draw_list, bb, flags, tab_col);
10599
if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10600
{
10601
// Might be moved to TabItemBackground() ?
10602
ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10603
ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale);
10604
ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline);
10605
if (style.TabRounding > 0.0f)
10606
{
10607
float rounding = style.TabRounding;
10608
display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9);
10609
display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11);
10610
display_draw_list->PathStroke(overline_col, 0, style.TabBarOverlineSize);
10611
}
10612
else
10613
{
10614
display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize);
10615
}
10616
}
10617
RenderNavCursor(bb, id);
10618
10619
// Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10620
const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10621
if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10622
TabBarQueueFocus(tab_bar, tab);
10623
10624
if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10625
flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10626
10627
// Render tab label, process close button
10628
const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0;
10629
bool just_closed;
10630
bool text_clipped;
10631
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);
10632
if (just_closed && p_open != NULL)
10633
{
10634
*p_open = false;
10635
TabBarCloseTab(tab_bar, tab);
10636
}
10637
10638
// Tooltip
10639
// (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10640
// (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10641
// FIXME: This is a mess.
10642
// FIXME: We may want disabled tab to still display the tooltip?
10643
if (text_clipped && g.HoveredId == id && !held)
10644
if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10645
SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10646
}
10647
10648
// Restore main window position so user can draw there
10649
if (want_clip_rect)
10650
PopClipRect();
10651
window->DC.CursorPos = backup_main_cursor_pos;
10652
10653
IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10654
if (is_tab_button)
10655
return pressed;
10656
return tab_contents_visible;
10657
}
10658
10659
// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10660
// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10661
// Tabs closed by the close button will automatically be flagged to avoid this issue.
10662
void ImGui::SetTabItemClosed(const char* label)
10663
{
10664
ImGuiContext& g = *GImGui;
10665
bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10666
if (is_within_manual_tab_bar)
10667
{
10668
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10669
ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10670
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10671
tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10672
}
10673
}
10674
10675
ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10676
{
10677
ImGuiContext& g = *GImGui;
10678
ImVec2 label_size = CalcTextSize(label, NULL, true);
10679
ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10680
if (has_close_button_or_unsaved_marker)
10681
size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10682
else
10683
size.x += g.Style.FramePadding.x + 1.0f;
10684
return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10685
}
10686
10687
ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*)
10688
{
10689
IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch.
10690
return ImVec2(0.0f, 0.0f);
10691
}
10692
10693
void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10694
{
10695
// 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.
10696
ImGuiContext& g = *GImGui;
10697
const float width = bb.GetWidth();
10698
IM_UNUSED(flags);
10699
IM_ASSERT(width > 0.0f);
10700
const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
10701
const float y1 = bb.Min.y + 1.0f;
10702
const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10703
draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
10704
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9);
10705
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12);
10706
draw_list->PathLineTo(ImVec2(bb.Max.x, y2));
10707
draw_list->PathFillConvex(col);
10708
if (g.Style.TabBorderSize > 0.0f)
10709
{
10710
draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
10711
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
10712
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
10713
draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
10714
draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize);
10715
}
10716
}
10717
10718
// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10719
// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10720
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)
10721
{
10722
ImGuiContext& g = *GImGui;
10723
ImVec2 label_size = CalcTextSize(label, NULL, true);
10724
10725
if (out_just_closed)
10726
*out_just_closed = false;
10727
if (out_text_clipped)
10728
*out_text_clipped = false;
10729
10730
if (bb.GetWidth() <= 1.0f)
10731
return;
10732
10733
// In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10734
// But right now if you want to alter text color of tabs this is what you need to do.
10735
#if 0
10736
const float backup_alpha = g.Style.Alpha;
10737
if (!is_contents_visible)
10738
g.Style.Alpha *= 0.7f;
10739
#endif
10740
10741
// Render text label (with clipping + alpha gradient) + unsaved marker
10742
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);
10743
10744
// Return clipped state ignoring the close button
10745
if (out_text_clipped)
10746
{
10747
*out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_ellipsis_clip_bb.Max.x;
10748
//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));
10749
}
10750
10751
const float button_sz = g.FontSize;
10752
const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10753
10754
// Close Button & Unsaved Marker
10755
// We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10756
// 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10757
// 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10758
// 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10759
bool close_button_pressed = false;
10760
bool close_button_visible = false;
10761
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.
10762
10763
if (close_button_id != 0)
10764
{
10765
if (is_contents_visible)
10766
close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected));
10767
else
10768
close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected));
10769
}
10770
10771
// When tabs/document is unsaved, the unsaved marker takes priority over the close button.
10772
const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered);
10773
if (unsaved_marker_visible)
10774
{
10775
ImVec2 bullet_pos = button_pos + ImVec2(button_sz, button_sz) * 0.5f;
10776
RenderBullet(draw_list, bullet_pos, GetColorU32(ImGuiCol_UnsavedMarker));
10777
}
10778
else if (close_button_visible)
10779
{
10780
ImGuiLastItemData last_item_backup = g.LastItemData;
10781
if (CloseButton(close_button_id, button_pos))
10782
close_button_pressed = true;
10783
g.LastItemData = last_item_backup;
10784
10785
// Close with middle mouse button
10786
if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
10787
close_button_pressed = true;
10788
}
10789
10790
// This is all rather complicated
10791
// (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10792
// 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..
10793
float ellipsis_max_x = text_ellipsis_clip_bb.Max.x;
10794
if (close_button_visible || unsaved_marker_visible)
10795
{
10796
const bool visible_without_hover = unsaved_marker_visible || (is_contents_visible ? g.Style.TabCloseButtonMinWidthSelected : g.Style.TabCloseButtonMinWidthUnselected) < 0.0f;
10797
if (visible_without_hover)
10798
{
10799
text_ellipsis_clip_bb.Max.x -= button_sz * 0.90f;
10800
ellipsis_max_x -= button_sz * 0.90f;
10801
}
10802
else
10803
{
10804
text_ellipsis_clip_bb.Max.x -= button_sz * 1.00f;
10805
}
10806
}
10807
LogSetNextTextDecoration("/", "\\");
10808
RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, ellipsis_max_x, label, NULL, &label_size);
10809
10810
#if 0
10811
if (!is_contents_visible)
10812
g.Style.Alpha = backup_alpha;
10813
#endif
10814
10815
if (out_just_closed)
10816
*out_just_closed = close_button_pressed;
10817
}
10818
10819
10820
#endif // #ifndef IMGUI_DISABLE
10821
10822