Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/ext/imgui/imgui_widgets.cpp
3186 views
1
// dear imgui, v1.91.6
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-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code.
74
#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness
75
#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used.
76
#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0
77
#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.
78
#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_')
79
#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
80
#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision
81
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access
82
#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type
83
#elif defined(__GNUC__)
84
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
85
#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked
86
#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
87
#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated
88
#endif
89
90
//-------------------------------------------------------------------------
91
// Data
92
//-------------------------------------------------------------------------
93
94
// Widgets
95
static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior.
96
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.
97
98
// Those MIN/MAX values are not define because we need to point to them
99
static const signed char IM_S8_MIN = -128;
100
static const signed char IM_S8_MAX = 127;
101
static const unsigned char IM_U8_MIN = 0;
102
static const unsigned char IM_U8_MAX = 0xFF;
103
static const signed short IM_S16_MIN = -32768;
104
static const signed short IM_S16_MAX = 32767;
105
static const unsigned short IM_U16_MIN = 0;
106
static const unsigned short IM_U16_MAX = 0xFFFF;
107
static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000);
108
static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF)
109
static const ImU32 IM_U32_MIN = 0;
110
static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF)
111
#ifdef LLONG_MIN
112
static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll);
113
static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll);
114
#else
115
static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1;
116
static const ImS64 IM_S64_MAX = 9223372036854775807LL;
117
#endif
118
static const ImU64 IM_U64_MIN = 0;
119
#ifdef ULLONG_MAX
120
static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull);
121
#else
122
static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1);
123
#endif
124
125
//-------------------------------------------------------------------------
126
// [SECTION] Forward Declarations
127
//-------------------------------------------------------------------------
128
129
// For InputTextEx()
130
static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false);
131
static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end);
132
static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false);
133
134
//-------------------------------------------------------------------------
135
// [SECTION] Widgets: Text, etc.
136
//-------------------------------------------------------------------------
137
// - TextEx() [Internal]
138
// - TextUnformatted()
139
// - Text()
140
// - TextV()
141
// - TextColored()
142
// - TextColoredV()
143
// - TextDisabled()
144
// - TextDisabledV()
145
// - TextWrapped()
146
// - TextWrappedV()
147
// - LabelText()
148
// - LabelTextV()
149
// - BulletText()
150
// - BulletTextV()
151
//-------------------------------------------------------------------------
152
153
void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags)
154
{
155
ImGuiWindow* window = GetCurrentWindow();
156
if (window->SkipItems)
157
return;
158
ImGuiContext& g = *GImGui;
159
160
// Accept null ranges
161
if (text == text_end)
162
text = text_end = "";
163
164
// Calculate length
165
const char* text_begin = text;
166
if (text_end == NULL)
167
text_end = text + strlen(text); // FIXME-OPT
168
169
const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
170
const float wrap_pos_x = window->DC.TextWrapPos;
171
const bool wrap_enabled = (wrap_pos_x >= 0.0f);
172
if (text_end - text <= 2000 || wrap_enabled)
173
{
174
// Common case
175
const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f;
176
const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width);
177
178
ImRect bb(text_pos, text_pos + text_size);
179
ItemSize(text_size, 0.0f);
180
if (!ItemAdd(bb, 0))
181
return;
182
183
// Render (we don't hide text after ## in this end-user function)
184
RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width);
185
}
186
else
187
{
188
// Long text!
189
// Perform manual coarse clipping to optimize for long multi-line text
190
// - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled.
191
// - 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.
192
// - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop.
193
const char* line = text;
194
const float line_height = GetTextLineHeight();
195
ImVec2 text_size(0, 0);
196
197
// Lines to skip (can't skip when logging text)
198
ImVec2 pos = text_pos;
199
if (!g.LogEnabled)
200
{
201
int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height);
202
if (lines_skippable > 0)
203
{
204
int lines_skipped = 0;
205
while (line < text_end && lines_skipped < lines_skippable)
206
{
207
const char* line_end = (const char*)memchr(line, '\n', text_end - line);
208
if (!line_end)
209
line_end = text_end;
210
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
211
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
212
line = line_end + 1;
213
lines_skipped++;
214
}
215
pos.y += lines_skipped * line_height;
216
}
217
}
218
219
// Lines to render
220
if (line < text_end)
221
{
222
ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height));
223
while (line < text_end)
224
{
225
if (IsClippedEx(line_rect, 0))
226
break;
227
228
const char* line_end = (const char*)memchr(line, '\n', text_end - line);
229
if (!line_end)
230
line_end = text_end;
231
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
232
RenderText(pos, line, line_end, false);
233
line = line_end + 1;
234
line_rect.Min.y += line_height;
235
line_rect.Max.y += line_height;
236
pos.y += line_height;
237
}
238
239
// Count remaining lines
240
int lines_skipped = 0;
241
while (line < text_end)
242
{
243
const char* line_end = (const char*)memchr(line, '\n', text_end - line);
244
if (!line_end)
245
line_end = text_end;
246
if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0)
247
text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x);
248
line = line_end + 1;
249
lines_skipped++;
250
}
251
pos.y += lines_skipped * line_height;
252
}
253
text_size.y = (pos - text_pos).y;
254
255
ImRect bb(text_pos, text_pos + text_size);
256
ItemSize(text_size, 0.0f);
257
ItemAdd(bb, 0);
258
}
259
}
260
261
void ImGui::TextUnformatted(const char* text, const char* text_end)
262
{
263
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
264
}
265
266
void ImGui::Text(const char* fmt, ...)
267
{
268
va_list args;
269
va_start(args, fmt);
270
TextV(fmt, args);
271
va_end(args);
272
}
273
274
void ImGui::TextV(const char* fmt, va_list args)
275
{
276
ImGuiWindow* window = GetCurrentWindow();
277
if (window->SkipItems)
278
return;
279
280
const char* text, *text_end;
281
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
282
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
283
}
284
285
void ImGui::TextColored(const ImVec4& col, const char* fmt, ...)
286
{
287
va_list args;
288
va_start(args, fmt);
289
TextColoredV(col, fmt, args);
290
va_end(args);
291
}
292
293
void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args)
294
{
295
PushStyleColor(ImGuiCol_Text, col);
296
TextV(fmt, args);
297
PopStyleColor();
298
}
299
300
void ImGui::TextDisabled(const char* fmt, ...)
301
{
302
va_list args;
303
va_start(args, fmt);
304
TextDisabledV(fmt, args);
305
va_end(args);
306
}
307
308
void ImGui::TextDisabledV(const char* fmt, va_list args)
309
{
310
ImGuiContext& g = *GImGui;
311
PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]);
312
TextV(fmt, args);
313
PopStyleColor();
314
}
315
316
void ImGui::TextWrapped(const char* fmt, ...)
317
{
318
va_list args;
319
va_start(args, fmt);
320
TextWrappedV(fmt, args);
321
va_end(args);
322
}
323
324
void ImGui::TextWrappedV(const char* fmt, va_list args)
325
{
326
ImGuiContext& g = *GImGui;
327
const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set
328
if (need_backup)
329
PushTextWrapPos(0.0f);
330
TextV(fmt, args);
331
if (need_backup)
332
PopTextWrapPos();
333
}
334
335
void ImGui::LabelText(const char* label, const char* fmt, ...)
336
{
337
va_list args;
338
va_start(args, fmt);
339
LabelTextV(label, fmt, args);
340
va_end(args);
341
}
342
343
// Add a label+text combo aligned to other label+value widgets
344
void ImGui::LabelTextV(const char* label, const char* fmt, va_list args)
345
{
346
ImGuiWindow* window = GetCurrentWindow();
347
if (window->SkipItems)
348
return;
349
350
ImGuiContext& g = *GImGui;
351
const ImGuiStyle& style = g.Style;
352
const float w = CalcItemWidth();
353
354
const char* value_text_begin, *value_text_end;
355
ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args);
356
const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false);
357
const ImVec2 label_size = CalcTextSize(label, NULL, true);
358
359
const ImVec2 pos = window->DC.CursorPos;
360
const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2));
361
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));
362
ItemSize(total_bb, style.FramePadding.y);
363
if (!ItemAdd(total_bb, 0))
364
return;
365
366
// Render
367
RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f));
368
if (label_size.x > 0.0f)
369
RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label);
370
}
371
372
void ImGui::BulletText(const char* fmt, ...)
373
{
374
va_list args;
375
va_start(args, fmt);
376
BulletTextV(fmt, args);
377
va_end(args);
378
}
379
380
// Text with a little bullet aligned to the typical tree node.
381
void ImGui::BulletTextV(const char* fmt, va_list args)
382
{
383
ImGuiWindow* window = GetCurrentWindow();
384
if (window->SkipItems)
385
return;
386
387
ImGuiContext& g = *GImGui;
388
const ImGuiStyle& style = g.Style;
389
390
const char* text_begin, *text_end;
391
ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args);
392
const ImVec2 label_size = CalcTextSize(text_begin, text_end, false);
393
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
394
ImVec2 pos = window->DC.CursorPos;
395
pos.y += window->DC.CurrLineTextBaseOffset;
396
ItemSize(total_size, 0.0f);
397
const ImRect bb(pos, pos + total_size);
398
if (!ItemAdd(bb, 0))
399
return;
400
401
// Render
402
ImU32 text_col = GetColorU32(ImGuiCol_Text);
403
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col);
404
RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false);
405
}
406
407
//-------------------------------------------------------------------------
408
// [SECTION] Widgets: Main
409
//-------------------------------------------------------------------------
410
// - ButtonBehavior() [Internal]
411
// - Button()
412
// - SmallButton()
413
// - InvisibleButton()
414
// - ArrowButton()
415
// - CloseButton() [Internal]
416
// - CollapseButton() [Internal]
417
// - GetWindowScrollbarID() [Internal]
418
// - GetWindowScrollbarRect() [Internal]
419
// - Scrollbar() [Internal]
420
// - ScrollbarEx() [Internal]
421
// - Image()
422
// - ImageButton()
423
// - Checkbox()
424
// - CheckboxFlagsT() [Internal]
425
// - CheckboxFlags()
426
// - RadioButton()
427
// - ProgressBar()
428
// - Bullet()
429
// - Hyperlink()
430
//-------------------------------------------------------------------------
431
432
// The ButtonBehavior() function is key to many interactions and used by many/most widgets.
433
// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_),
434
// this code is a little complex.
435
// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior.
436
// See the series of events below and the corresponding state reported by dear imgui:
437
//------------------------------------------------------------------------------------------------------------------------------------------------
438
// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
439
// Frame N+0 (mouse is outside bb) - - - - - -
440
// Frame N+1 (mouse moves inside bb) - true - - - -
441
// Frame N+2 (mouse button is down) - true true true - true
442
// Frame N+3 (mouse button is down) - true true - - -
443
// Frame N+4 (mouse moves outside bb) - - true - - -
444
// Frame N+5 (mouse moves inside bb) - true true - - -
445
// Frame N+6 (mouse button is released) true true - - true -
446
// Frame N+7 (mouse button is released) - true - - - -
447
// Frame N+8 (mouse moves outside bb) - - - - - -
448
//------------------------------------------------------------------------------------------------------------------------------------------------
449
// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
450
// Frame N+2 (mouse button is down) true true true true - true
451
// Frame N+3 (mouse button is down) - true true - - -
452
// Frame N+6 (mouse button is released) - true - - true -
453
// Frame N+7 (mouse button is released) - true - - - -
454
//------------------------------------------------------------------------------------------------------------------------------------------------
455
// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
456
// Frame N+2 (mouse button is down) - true - - - true
457
// Frame N+3 (mouse button is down) - true - - - -
458
// Frame N+6 (mouse button is released) true true - - - -
459
// Frame N+7 (mouse button is released) - true - - - -
460
//------------------------------------------------------------------------------------------------------------------------------------------------
461
// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked()
462
// Frame N+0 (mouse button is down) - true - - - true
463
// Frame N+1 (mouse button is down) - true - - - -
464
// Frame N+2 (mouse button is released) - true - - - -
465
// Frame N+3 (mouse button is released) - true - - - -
466
// Frame N+4 (mouse button is down) true true true true - true
467
// Frame N+5 (mouse button is down) - true true - - -
468
// Frame N+6 (mouse button is released) - true - - true -
469
// Frame N+7 (mouse button is released) - true - - - -
470
//------------------------------------------------------------------------------------------------------------------------------------------------
471
// Note that some combinations are supported,
472
// - PressedOnDragDropHold can generally be associated with any flag.
473
// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported.
474
//------------------------------------------------------------------------------------------------------------------------------------------------
475
// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set:
476
// Repeat+ Repeat+ Repeat+ Repeat+
477
// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick
478
//-------------------------------------------------------------------------------------------------------------------------------------------------
479
// Frame N+0 (mouse button is down) - true - true
480
// ... - - - -
481
// Frame N + RepeatDelay true true - true
482
// ... - - - -
483
// Frame N + RepeatDelay + RepeatRate*N true true - true
484
//-------------------------------------------------------------------------------------------------------------------------------------------------
485
486
// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc.
487
// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);'
488
// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading.
489
// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature.
490
// One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior()
491
// with same ID and different MouseButton (see #8030). You can fix it by:
492
// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags.
493
// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()
494
bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags)
495
{
496
ImGuiContext& g = *GImGui;
497
ImGuiWindow* window = GetCurrentWindow();
498
499
// Default behavior inherited from item flags
500
// Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that.
501
ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags);
502
if (flags & ImGuiButtonFlags_AllowOverlap)
503
item_flags |= ImGuiItemFlags_AllowOverlap;
504
505
// Default only reacts to left mouse button
506
if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0)
507
flags |= ImGuiButtonFlags_MouseButtonLeft;
508
509
// Default behavior requires click + release inside bounding box
510
if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0)
511
flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_;
512
513
ImGuiWindow* backup_hovered_window = g.HoveredWindow;
514
const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree;
515
if (flatten_hovered_children)
516
g.HoveredWindow = window;
517
518
#ifdef IMGUI_ENABLE_TEST_ENGINE
519
// Alternate registration spot, for when caller didn't use ItemAdd()
520
if (g.LastItemData.ID != id)
521
IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL);
522
#endif
523
524
bool pressed = false;
525
bool hovered = ItemHoverable(bb, id, item_flags);
526
527
// Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button
528
if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers))
529
if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
530
{
531
hovered = true;
532
SetHoveredID(id);
533
if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER)
534
{
535
pressed = true;
536
g.DragDropHoldJustPressedId = id;
537
FocusWindow(window);
538
}
539
}
540
541
if (flatten_hovered_children)
542
g.HoveredWindow = backup_hovered_window;
543
544
// Mouse handling
545
const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id;
546
if (hovered)
547
{
548
IM_ASSERT(id != 0); // Lazily check inside rare path.
549
550
// Poll mouse buttons
551
// - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId.
552
// - Technically we only need some values in one code path, but since this is gated by hovered test this is fine.
553
int mouse_button_clicked = -1;
554
int mouse_button_released = -1;
555
for (int button = 0; button < 3; button++)
556
if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here.
557
{
558
if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; }
559
if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; }
560
}
561
562
// Process initial action
563
const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt);
564
if (mods_ok)
565
{
566
if (mouse_button_clicked != -1 && g.ActiveId != id)
567
{
568
if (!(flags & ImGuiButtonFlags_NoSetKeyOwner))
569
SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id);
570
if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere))
571
{
572
SetActiveID(id, window);
573
g.ActiveIdMouseButton = mouse_button_clicked;
574
if (!(flags & ImGuiButtonFlags_NoNavFocus))
575
{
576
SetFocusID(id, window);
577
FocusWindow(window);
578
}
579
else
580
{
581
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
582
}
583
}
584
if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2))
585
{
586
pressed = true;
587
if (flags & ImGuiButtonFlags_NoHoldingActiveId)
588
ClearActiveID();
589
else
590
SetActiveID(id, window); // Hold on ID
591
g.ActiveIdMouseButton = mouse_button_clicked;
592
if (!(flags & ImGuiButtonFlags_NoNavFocus))
593
{
594
SetFocusID(id, window);
595
FocusWindow(window);
596
}
597
else
598
{
599
FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child
600
}
601
}
602
}
603
if (flags & ImGuiButtonFlags_PressedOnRelease)
604
{
605
if (mouse_button_released != -1)
606
{
607
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
608
if (!has_repeated_at_least_once)
609
pressed = true;
610
if (!(flags & ImGuiButtonFlags_NoNavFocus))
611
SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why.
612
ClearActiveID();
613
}
614
}
615
616
// 'Repeat' mode acts when held regardless of _PressedOn flags (see table above).
617
// Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings.
618
if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat))
619
if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id))
620
pressed = true;
621
}
622
623
if (pressed && g.IO.ConfigNavCursorVisibleAuto)
624
g.NavCursorVisible = false;
625
}
626
627
// Keyboard/Gamepad navigation handling
628
// We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse.
629
if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav)
630
if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus))
631
hovered = true;
632
if (g.NavActivateDownId == id)
633
{
634
bool nav_activated_by_code = (g.NavActivateId == id);
635
bool nav_activated_by_inputs = (g.NavActivatePressedId == id);
636
if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat))
637
{
638
// Avoid pressing multiple keys from triggering excessive amount of repeat events
639
const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space);
640
const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter);
641
const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate);
642
const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration);
643
nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0;
644
}
645
if (nav_activated_by_code || nav_activated_by_inputs)
646
{
647
// Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button.
648
pressed = true;
649
SetActiveID(id, window);
650
g.ActiveIdSource = g.NavInputSource;
651
if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut))
652
SetFocusID(id, window);
653
if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)
654
g.ActiveIdFromShortcut = true;
655
}
656
}
657
658
// Process while held
659
bool held = false;
660
if (g.ActiveId == id)
661
{
662
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
663
{
664
if (g.ActiveIdIsJustActivated)
665
g.ActiveIdClickOffset = g.IO.MousePos - bb.Min;
666
667
const int mouse_button = g.ActiveIdMouseButton;
668
if (mouse_button == -1)
669
{
670
// Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304).
671
ClearActiveID();
672
}
673
else if (IsMouseDown(mouse_button, test_owner_id))
674
{
675
held = true;
676
}
677
else
678
{
679
bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0;
680
bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0;
681
if ((release_in || release_anywhere) && !g.DragDropActive)
682
{
683
// Report as pressed when releasing the mouse (this is the most common path)
684
bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2;
685
bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps <on release>
686
bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id);
687
if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned)
688
pressed = true;
689
}
690
ClearActiveID();
691
}
692
if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto)
693
g.NavCursorVisible = false;
694
}
695
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
696
{
697
// When activated using Nav, we hold on the ActiveID until activation button is released
698
if (g.NavActivateDownId == id)
699
held = true; // hovered == true not true as we are already likely hovered on direct activation.
700
else
701
ClearActiveID();
702
}
703
if (pressed)
704
g.ActiveIdHasBeenPressedBefore = true;
705
}
706
707
// Activation highlight (this may be a remote activation)
708
if (g.NavHighlightActivatedId == id)
709
hovered = true;
710
711
if (out_hovered) *out_hovered = hovered;
712
if (out_held) *out_held = held;
713
714
return pressed;
715
}
716
717
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
718
{
719
ImGuiWindow* window = GetCurrentWindow();
720
if (window->SkipItems)
721
return false;
722
723
ImGuiContext& g = *GImGui;
724
const ImGuiStyle& style = g.Style;
725
const ImGuiID id = window->GetID(label);
726
const ImVec2 label_size = CalcTextSize(label, NULL, true);
727
728
ImVec2 pos = window->DC.CursorPos;
729
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)
730
pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y;
731
ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f);
732
733
const ImRect bb(pos, pos + size);
734
ItemSize(size, style.FramePadding.y);
735
if (!ItemAdd(bb, id))
736
return false;
737
738
bool hovered, held;
739
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
740
741
// Render
742
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
743
RenderNavCursor(bb, id);
744
RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding);
745
746
if (g.LogEnabled)
747
LogSetNextTextDecoration("[", "]");
748
RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb);
749
750
// Automatically close popups
751
//if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup))
752
// CloseCurrentPopup();
753
754
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
755
return pressed;
756
}
757
758
bool ImGui::Button(const char* label, const ImVec2& size_arg)
759
{
760
return ButtonEx(label, size_arg, ImGuiButtonFlags_None);
761
}
762
763
// Small buttons fits within text without additional vertical spacing.
764
bool ImGui::SmallButton(const char* label)
765
{
766
ImGuiContext& g = *GImGui;
767
float backup_padding_y = g.Style.FramePadding.y;
768
g.Style.FramePadding.y = 0.0f;
769
bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine);
770
g.Style.FramePadding.y = backup_padding_y;
771
return pressed;
772
}
773
774
// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack.
775
// 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)
776
bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags)
777
{
778
ImGuiContext& g = *GImGui;
779
ImGuiWindow* window = GetCurrentWindow();
780
if (window->SkipItems)
781
return false;
782
783
// Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size.
784
IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f);
785
786
const ImGuiID id = window->GetID(str_id);
787
ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f);
788
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
789
ItemSize(size);
790
if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav))
791
return false;
792
793
bool hovered, held;
794
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
795
RenderNavCursor(bb, id);
796
797
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
798
return pressed;
799
}
800
801
bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags)
802
{
803
ImGuiContext& g = *GImGui;
804
ImGuiWindow* window = GetCurrentWindow();
805
if (window->SkipItems)
806
return false;
807
808
const ImGuiID id = window->GetID(str_id);
809
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
810
const float default_size = GetFrameHeight();
811
ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f);
812
if (!ItemAdd(bb, id))
813
return false;
814
815
bool hovered, held;
816
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
817
818
// Render
819
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
820
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
821
RenderNavCursor(bb, id);
822
RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
823
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);
824
825
IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags);
826
return pressed;
827
}
828
829
bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir)
830
{
831
float sz = GetFrameHeight();
832
return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None);
833
}
834
835
// Button to close a window
836
bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)
837
{
838
ImGuiContext& g = *GImGui;
839
ImGuiWindow* window = g.CurrentWindow;
840
841
// 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)
842
// 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?
843
const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
844
ImRect bb_interact = bb;
845
const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea();
846
if (area_to_visible_ratio < 1.5f)
847
bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f));
848
849
// Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window.
850
// (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).
851
bool is_clipped = !ItemAdd(bb_interact, id);
852
853
bool hovered, held;
854
bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held);
855
if (is_clipped)
856
return pressed;
857
858
// Render
859
ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
860
if (hovered)
861
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
862
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
863
ImU32 cross_col = GetColorU32(ImGuiCol_Text);
864
ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f);
865
float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f;
866
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f);
867
window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f);
868
869
return pressed;
870
}
871
872
// The Collapse button also functions as a Dock Menu button.
873
bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node)
874
{
875
ImGuiContext& g = *GImGui;
876
ImGuiWindow* window = g.CurrentWindow;
877
878
ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize));
879
bool is_clipped = !ItemAdd(bb, id);
880
bool hovered, held;
881
bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None);
882
if (is_clipped)
883
return pressed;
884
885
// Render
886
//bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed);
887
ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
888
ImU32 text_col = GetColorU32(ImGuiCol_Text);
889
if (hovered || held)
890
window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col);
891
RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact);
892
893
if (dock_node)
894
RenderArrowDockMenu(window->DrawList, bb.Min, g.FontSize, text_col);
895
else
896
RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f);
897
898
// Switch to moving the window after mouse is moved beyond the initial drag threshold
899
if (IsItemActive() && IsMouseDragging(0))
900
StartMouseMovingWindowOrNode(window, dock_node, true); // Undock from window/collapse menu button
901
902
return pressed;
903
}
904
905
ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis)
906
{
907
return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY");
908
}
909
910
// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set.
911
ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis)
912
{
913
const ImRect outer_rect = window->Rect();
914
const ImRect inner_rect = window->InnerRect;
915
const float border_size = window->WindowBorderSize;
916
const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar)
917
IM_ASSERT(scrollbar_size > 0.0f);
918
if (axis == ImGuiAxis_X)
919
return ImRect(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size);
920
else
921
return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size);
922
}
923
924
void ImGui::Scrollbar(ImGuiAxis axis)
925
{
926
ImGuiContext& g = *GImGui;
927
ImGuiWindow* window = g.CurrentWindow;
928
const ImGuiID id = GetWindowScrollbarID(window, axis);
929
930
// Calculate scrollbar bounding box
931
ImRect bb = GetWindowScrollbarRect(window, axis);
932
ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone;
933
if (axis == ImGuiAxis_X)
934
{
935
rounding_corners |= ImDrawFlags_RoundCornersBottomLeft;
936
if (!window->ScrollbarY)
937
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
938
}
939
else
940
{
941
if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar))
942
rounding_corners |= ImDrawFlags_RoundCornersTopRight;
943
if (!window->ScrollbarX)
944
rounding_corners |= ImDrawFlags_RoundCornersBottomRight;
945
}
946
float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis];
947
float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f;
948
ImS64 scroll = (ImS64)window->Scroll[axis];
949
ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners);
950
window->Scroll[axis] = (float)scroll;
951
}
952
953
// Vertical/Horizontal scrollbar
954
// The entire piece of code below is rather confusing because:
955
// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab)
956
// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar
957
// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal.
958
// Still, the code should probably be made simpler..
959
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)
960
{
961
ImGuiContext& g = *GImGui;
962
ImGuiWindow* window = g.CurrentWindow;
963
if (window->SkipItems)
964
return false;
965
966
const float bb_frame_width = bb_frame.GetWidth();
967
const float bb_frame_height = bb_frame.GetHeight();
968
if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f)
969
return false;
970
971
// 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)
972
float alpha = 1.0f;
973
if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f)
974
alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f));
975
if (alpha <= 0.0f)
976
return false;
977
978
const ImGuiStyle& style = g.Style;
979
const bool allow_interaction = (alpha >= 1.0f);
980
981
ImRect bb = bb_frame;
982
bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f)));
983
984
// V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar)
985
const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight();
986
987
// Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount)
988
// But we maintain a minimum size in pixel to allow for the user to still aim inside.
989
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.
990
const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1);
991
const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), style.GrabMinSize, scrollbar_size_v);
992
const float grab_h_norm = grab_h_pixels / scrollbar_size_v;
993
994
// Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar().
995
bool held = false;
996
bool hovered = false;
997
ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav);
998
ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus);
999
1000
const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v);
1001
float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1002
float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space
1003
if (held && allow_interaction && grab_h_norm < 1.0f)
1004
{
1005
const float scrollbar_pos_v = bb.Min[axis];
1006
const float mouse_pos_v = g.IO.MousePos[axis];
1007
1008
// Click position in scrollbar normalized space (0.0f->1.0f)
1009
const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v);
1010
1011
const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0;
1012
if (g.ActiveIdIsJustActivated)
1013
{
1014
// On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab
1015
const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0);
1016
g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir;
1017
g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f;
1018
}
1019
1020
// Apply scroll (p_scroll_v will generally point on one member of window->Scroll)
1021
// 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
1022
if (g.ScrollbarSeekMode == 0)
1023
{
1024
// Absolute seeking
1025
const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm));
1026
*p_scroll_v = (ImS64)(scroll_v_norm * scroll_max);
1027
}
1028
else
1029
{
1030
// Page by page
1031
if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode)
1032
{
1033
float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f;
1034
*p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max);
1035
}
1036
}
1037
1038
// Update values for rendering
1039
scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max);
1040
grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v;
1041
1042
// Update distance to grab now that we have seek'ed and saturated
1043
//if (seek_absolute)
1044
// g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f;
1045
}
1046
1047
// Render
1048
const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg);
1049
const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha);
1050
window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags);
1051
ImRect grab_rect;
1052
if (axis == ImGuiAxis_X)
1053
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);
1054
else
1055
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);
1056
window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding);
1057
1058
return held;
1059
}
1060
1061
// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
1062
// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above.
1063
void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col)
1064
{
1065
ImGuiWindow* window = GetCurrentWindow();
1066
if (window->SkipItems)
1067
return;
1068
1069
const float border_size = (border_col.w > 0.0f) ? 1.0f : 0.0f;
1070
const ImVec2 padding(border_size, border_size);
1071
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1072
ItemSize(bb);
1073
if (!ItemAdd(bb, 0))
1074
return;
1075
1076
// Render
1077
if (border_size > 0.0f)
1078
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f, ImDrawFlags_None, border_size);
1079
window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1080
}
1081
1082
bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags)
1083
{
1084
ImGuiContext& g = *GImGui;
1085
ImGuiWindow* window = GetCurrentWindow();
1086
if (window->SkipItems)
1087
return false;
1088
1089
const ImVec2 padding = g.Style.FramePadding;
1090
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f);
1091
ItemSize(bb);
1092
if (!ItemAdd(bb, id))
1093
return false;
1094
1095
bool hovered, held;
1096
bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags);
1097
1098
// Render
1099
const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1100
RenderNavCursor(bb, id);
1101
RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding));
1102
if (bg_col.w > 0.0f)
1103
window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col));
1104
window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col));
1105
1106
return pressed;
1107
}
1108
1109
// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button.
1110
// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design?
1111
bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col)
1112
{
1113
ImGuiContext& g = *GImGui;
1114
ImGuiWindow* window = g.CurrentWindow;
1115
if (window->SkipItems)
1116
return false;
1117
1118
return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col);
1119
}
1120
1121
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1122
// Legacy API obsoleted in 1.89. Two differences with new ImageButton()
1123
// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID)
1124
// - new ImageButton() requires an explicit 'const char* str_id'
1125
// - old ImageButton() had frame_padding' override argument.
1126
// - new ImageButton() always use style.FramePadding.
1127
/*
1128
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)
1129
{
1130
// Default to using texture ID as ID. User can still push string/integer prefixes.
1131
PushID((ImTextureID)(intptr_t)user_texture_id);
1132
if (frame_padding >= 0)
1133
PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding));
1134
bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col);
1135
if (frame_padding >= 0)
1136
PopStyleVar();
1137
PopID();
1138
return ret;
1139
}
1140
*/
1141
#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1142
1143
bool ImGui::Checkbox(const char* label, bool* v)
1144
{
1145
ImGuiWindow* window = GetCurrentWindow();
1146
if (window->SkipItems)
1147
return false;
1148
1149
ImGuiContext& g = *GImGui;
1150
const ImGuiStyle& style = g.Style;
1151
const ImGuiID id = window->GetID(label);
1152
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1153
1154
const float square_sz = GetFrameHeight();
1155
const ImVec2 pos = window->DC.CursorPos;
1156
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));
1157
ItemSize(total_bb, style.FramePadding.y);
1158
const bool is_visible = ItemAdd(total_bb, id);
1159
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
1160
if (!is_visible)
1161
if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support
1162
{
1163
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1164
return false;
1165
}
1166
1167
// Range-Selection/Multi-selection support (header)
1168
bool checked = *v;
1169
if (is_multi_select)
1170
MultiSelectItemHeader(id, &checked, NULL);
1171
1172
bool hovered, held;
1173
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1174
1175
// Range-Selection/Multi-selection support (footer)
1176
if (is_multi_select)
1177
MultiSelectItemFooter(id, &checked, &pressed);
1178
else if (pressed)
1179
checked = !checked;
1180
1181
if (*v != checked)
1182
{
1183
*v = checked;
1184
pressed = true; // return value
1185
MarkItemEdited(id);
1186
}
1187
1188
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1189
const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0;
1190
if (is_visible)
1191
{
1192
RenderNavCursor(total_bb, id);
1193
RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding);
1194
ImU32 check_col = GetColorU32(ImGuiCol_CheckMark);
1195
if (mixed_value)
1196
{
1197
// Undocumented tristate/mixed/indeterminate checkbox (#2644)
1198
// This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox)
1199
ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)));
1200
window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding);
1201
}
1202
else if (*v)
1203
{
1204
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1205
RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f);
1206
}
1207
}
1208
const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1209
if (g.LogEnabled)
1210
LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]");
1211
if (is_visible && label_size.x > 0.0f)
1212
RenderText(label_pos, label);
1213
1214
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
1215
return pressed;
1216
}
1217
1218
template<typename T>
1219
bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value)
1220
{
1221
bool all_on = (*flags & flags_value) == flags_value;
1222
bool any_on = (*flags & flags_value) != 0;
1223
bool pressed;
1224
if (!all_on && any_on)
1225
{
1226
ImGuiContext& g = *GImGui;
1227
g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue;
1228
pressed = Checkbox(label, &all_on);
1229
}
1230
else
1231
{
1232
pressed = Checkbox(label, &all_on);
1233
1234
}
1235
if (pressed)
1236
{
1237
if (all_on)
1238
*flags |= flags_value;
1239
else
1240
*flags &= ~flags_value;
1241
}
1242
return pressed;
1243
}
1244
1245
bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value)
1246
{
1247
return CheckboxFlagsT(label, flags, flags_value);
1248
}
1249
1250
bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value)
1251
{
1252
return CheckboxFlagsT(label, flags, flags_value);
1253
}
1254
1255
bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value)
1256
{
1257
return CheckboxFlagsT(label, flags, flags_value);
1258
}
1259
1260
bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value)
1261
{
1262
return CheckboxFlagsT(label, flags, flags_value);
1263
}
1264
1265
bool ImGui::RadioButton(const char* label, bool active)
1266
{
1267
ImGuiWindow* window = GetCurrentWindow();
1268
if (window->SkipItems)
1269
return false;
1270
1271
ImGuiContext& g = *GImGui;
1272
const ImGuiStyle& style = g.Style;
1273
const ImGuiID id = window->GetID(label);
1274
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1275
1276
const float square_sz = GetFrameHeight();
1277
const ImVec2 pos = window->DC.CursorPos;
1278
const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz));
1279
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));
1280
ItemSize(total_bb, style.FramePadding.y);
1281
if (!ItemAdd(total_bb, id))
1282
return false;
1283
1284
ImVec2 center = check_bb.GetCenter();
1285
center.x = IM_ROUND(center.x);
1286
center.y = IM_ROUND(center.y);
1287
const float radius = (square_sz - 1.0f) * 0.5f;
1288
1289
bool hovered, held;
1290
bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
1291
if (pressed)
1292
MarkItemEdited(id);
1293
1294
RenderNavCursor(total_bb, id);
1295
const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius);
1296
window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment);
1297
if (active)
1298
{
1299
const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f));
1300
window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark));
1301
}
1302
1303
if (style.FrameBorderSize > 0.0f)
1304
{
1305
window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize);
1306
window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize);
1307
}
1308
1309
ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y);
1310
if (g.LogEnabled)
1311
LogRenderedText(&label_pos, active ? "(x)" : "( )");
1312
if (label_size.x > 0.0f)
1313
RenderText(label_pos, label);
1314
1315
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1316
return pressed;
1317
}
1318
1319
// 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..
1320
bool ImGui::RadioButton(const char* label, int* v, int v_button)
1321
{
1322
const bool pressed = RadioButton(label, *v == v_button);
1323
if (pressed)
1324
*v = v_button;
1325
return pressed;
1326
}
1327
1328
// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size
1329
void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay)
1330
{
1331
ImGuiWindow* window = GetCurrentWindow();
1332
if (window->SkipItems)
1333
return;
1334
1335
ImGuiContext& g = *GImGui;
1336
const ImGuiStyle& style = g.Style;
1337
1338
ImVec2 pos = window->DC.CursorPos;
1339
ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f);
1340
ImRect bb(pos, pos + size);
1341
ItemSize(size, style.FramePadding.y);
1342
if (!ItemAdd(bb, 0))
1343
return;
1344
1345
// Fraction < 0.0f will display an indeterminate progress bar animation
1346
// The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works.
1347
const bool is_indeterminate = (fraction < 0.0f);
1348
if (!is_indeterminate)
1349
fraction = ImSaturate(fraction);
1350
1351
// Out of courtesy we accept a NaN fraction without crashing
1352
float fill_n0 = 0.0f;
1353
float fill_n1 = (fraction == fraction) ? fraction : 0.0f;
1354
1355
if (is_indeterminate)
1356
{
1357
const float fill_width_n = 0.2f;
1358
fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n;
1359
fill_n1 = ImSaturate(fill_n0 + fill_width_n);
1360
fill_n0 = ImSaturate(fill_n0);
1361
}
1362
1363
// Render
1364
RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
1365
bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize));
1366
RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding);
1367
1368
// Default displaying the fraction as percentage string, but user can override it
1369
// Don't display text for indeterminate bars by default
1370
char overlay_buf[32];
1371
if (!is_indeterminate || overlay != NULL)
1372
{
1373
if (!overlay)
1374
{
1375
ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f);
1376
overlay = overlay_buf;
1377
}
1378
1379
ImVec2 overlay_size = CalcTextSize(overlay, NULL);
1380
if (overlay_size.x > 0.0f)
1381
{
1382
float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x;
1383
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);
1384
}
1385
}
1386
}
1387
1388
void ImGui::Bullet()
1389
{
1390
ImGuiWindow* window = GetCurrentWindow();
1391
if (window->SkipItems)
1392
return;
1393
1394
ImGuiContext& g = *GImGui;
1395
const ImGuiStyle& style = g.Style;
1396
const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize);
1397
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height));
1398
ItemSize(bb);
1399
if (!ItemAdd(bb, 0))
1400
{
1401
SameLine(0, style.FramePadding.x * 2);
1402
return;
1403
}
1404
1405
// Render and stay on same line
1406
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1407
RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col);
1408
SameLine(0, style.FramePadding.x * 2.0f);
1409
}
1410
1411
// This is provided as a convenience for being an often requested feature.
1412
// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system.
1413
// Because of this we currently don't provide many styling options for this widget
1414
// (e.g. hovered/active colors are automatically inferred from a single color).
1415
bool ImGui::TextLink(const char* label)
1416
{
1417
ImGuiWindow* window = GetCurrentWindow();
1418
if (window->SkipItems)
1419
return false;
1420
1421
ImGuiContext& g = *GImGui;
1422
const ImGuiID id = window->GetID(label);
1423
const char* label_end = FindRenderedTextEnd(label);
1424
1425
ImVec2 pos = window->DC.CursorPos;
1426
ImVec2 size = CalcTextSize(label, label_end, true);
1427
ImRect bb(pos, pos + size);
1428
ItemSize(size, 0.0f);
1429
if (!ItemAdd(bb, id))
1430
return false;
1431
1432
bool hovered, held;
1433
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1434
RenderNavCursor(bb, id);
1435
1436
if (hovered)
1437
SetMouseCursor(ImGuiMouseCursor_Hand);
1438
1439
ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink];
1440
ImVec4 line_colf = text_colf;
1441
{
1442
// FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets,
1443
// as we are currently experimenting/planning a different styling system.
1444
float h, s, v;
1445
ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v);
1446
if (held || hovered)
1447
{
1448
v = ImSaturate(v + (held ? 0.4f : 0.3f));
1449
h = ImFmod(h + 0.02f, 1.0f);
1450
}
1451
ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z);
1452
v = ImSaturate(v - 0.20f);
1453
ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z);
1454
}
1455
1456
float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f);
1457
window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode.
1458
1459
PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf));
1460
RenderText(bb.Min, label, label_end);
1461
PopStyleColor();
1462
1463
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
1464
return pressed;
1465
}
1466
1467
void ImGui::TextLinkOpenURL(const char* label, const char* url)
1468
{
1469
ImGuiContext& g = *GImGui;
1470
if (url == NULL)
1471
url = label;
1472
if (TextLink(label))
1473
if (g.PlatformIO.Platform_OpenInShellFn != NULL)
1474
g.PlatformIO.Platform_OpenInShellFn(&g, url);
1475
SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label
1476
if (BeginPopupContextItem())
1477
{
1478
if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink)))
1479
SetClipboardText(url);
1480
EndPopup();
1481
}
1482
}
1483
1484
//-------------------------------------------------------------------------
1485
// [SECTION] Widgets: Low-level Layout helpers
1486
//-------------------------------------------------------------------------
1487
// - Spacing()
1488
// - Dummy()
1489
// - NewLine()
1490
// - AlignTextToFramePadding()
1491
// - SeparatorEx() [Internal]
1492
// - Separator()
1493
// - SplitterBehavior() [Internal]
1494
// - ShrinkWidths() [Internal]
1495
//-------------------------------------------------------------------------
1496
1497
void ImGui::Spacing()
1498
{
1499
ImGuiWindow* window = GetCurrentWindow();
1500
if (window->SkipItems)
1501
return;
1502
ItemSize(ImVec2(0, 0));
1503
}
1504
1505
void ImGui::Dummy(const ImVec2& size)
1506
{
1507
ImGuiWindow* window = GetCurrentWindow();
1508
if (window->SkipItems)
1509
return;
1510
1511
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
1512
ItemSize(size);
1513
ItemAdd(bb, 0);
1514
}
1515
1516
void ImGui::NewLine()
1517
{
1518
ImGuiWindow* window = GetCurrentWindow();
1519
if (window->SkipItems)
1520
return;
1521
1522
ImGuiContext& g = *GImGui;
1523
const ImGuiLayoutType backup_layout_type = window->DC.LayoutType;
1524
window->DC.LayoutType = ImGuiLayoutType_Vertical;
1525
window->DC.IsSameLine = false;
1526
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.
1527
ItemSize(ImVec2(0, 0));
1528
else
1529
ItemSize(ImVec2(0.0f, g.FontSize));
1530
window->DC.LayoutType = backup_layout_type;
1531
}
1532
1533
void ImGui::AlignTextToFramePadding()
1534
{
1535
ImGuiWindow* window = GetCurrentWindow();
1536
if (window->SkipItems)
1537
return;
1538
1539
ImGuiContext& g = *GImGui;
1540
window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2);
1541
window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y);
1542
}
1543
1544
// Horizontal/vertical separating line
1545
// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues.
1546
// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are.
1547
void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness)
1548
{
1549
ImGuiWindow* window = GetCurrentWindow();
1550
if (window->SkipItems)
1551
return;
1552
1553
ImGuiContext& g = *GImGui;
1554
IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected
1555
IM_ASSERT(thickness > 0.0f);
1556
1557
if (flags & ImGuiSeparatorFlags_Vertical)
1558
{
1559
// Vertical separator, for menu bars (use current line height).
1560
float y1 = window->DC.CursorPos.y;
1561
float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y;
1562
const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2));
1563
ItemSize(ImVec2(thickness, 0.0f));
1564
if (!ItemAdd(bb, 0))
1565
return;
1566
1567
// Draw
1568
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1569
if (g.LogEnabled)
1570
LogText(" |");
1571
}
1572
else if (flags & ImGuiSeparatorFlags_Horizontal)
1573
{
1574
// Horizontal Separator
1575
float x1 = window->DC.CursorPos.x;
1576
float x2 = window->WorkRect.Max.x;
1577
1578
// Preserve legacy behavior inside Columns()
1579
// Before Tables API happened, we relied on Separator() to span all columns of a Columns() set.
1580
// We currently don't need to provide the same feature for tables because tables naturally have border features.
1581
ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL;
1582
if (columns)
1583
{
1584
x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03
1585
x2 = window->Pos.x + window->Size.x;
1586
PushColumnsBackground();
1587
}
1588
1589
// We don't provide our width to the layout so that it doesn't get feed back into AutoFit
1590
// FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell)
1591
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.
1592
const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness));
1593
ItemSize(ImVec2(0.0f, thickness_for_layout));
1594
1595
if (ItemAdd(bb, 0))
1596
{
1597
// Draw
1598
window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator));
1599
if (g.LogEnabled)
1600
LogRenderedText(&bb.Min, "--------------------------------\n");
1601
1602
}
1603
if (columns)
1604
{
1605
PopColumnsBackground();
1606
columns->LineMinY = window->DC.CursorPos.y;
1607
}
1608
}
1609
}
1610
1611
void ImGui::Separator()
1612
{
1613
ImGuiContext& g = *GImGui;
1614
ImGuiWindow* window = g.CurrentWindow;
1615
if (window->SkipItems)
1616
return;
1617
1618
// Those flags should eventually be configurable by the user
1619
// FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f.
1620
ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal;
1621
1622
// Only applies to legacy Columns() api as they relied on Separator() a lot.
1623
if (window->DC.CurrentColumns)
1624
flags |= ImGuiSeparatorFlags_SpanAllColumns;
1625
1626
SeparatorEx(flags, 1.0f);
1627
}
1628
1629
void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w)
1630
{
1631
ImGuiContext& g = *GImGui;
1632
ImGuiWindow* window = g.CurrentWindow;
1633
ImGuiStyle& style = g.Style;
1634
1635
const ImVec2 label_size = CalcTextSize(label, label_end, false);
1636
const ImVec2 pos = window->DC.CursorPos;
1637
const ImVec2 padding = style.SeparatorTextPadding;
1638
1639
const float separator_thickness = style.SeparatorTextBorderSize;
1640
const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness));
1641
const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y));
1642
const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f));
1643
ItemSize(min_size, text_baseline_y);
1644
if (!ItemAdd(bb, id))
1645
return;
1646
1647
const float sep1_x1 = pos.x;
1648
const float sep2_x2 = bb.Max.x;
1649
const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f);
1650
1651
const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f);
1652
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
1653
1654
// This allows using SameLine() to position something in the 'extra_w'
1655
window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x;
1656
1657
const ImU32 separator_col = GetColorU32(ImGuiCol_Separator);
1658
if (label_size.x > 0.0f)
1659
{
1660
const float sep1_x2 = label_pos.x - style.ItemSpacing.x;
1661
const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x;
1662
if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f)
1663
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness);
1664
if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f)
1665
window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1666
if (g.LogEnabled)
1667
LogSetNextTextDecoration("---", NULL);
1668
RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size);
1669
}
1670
else
1671
{
1672
if (g.LogEnabled)
1673
LogText("---");
1674
if (separator_thickness > 0.0f)
1675
window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness);
1676
}
1677
}
1678
1679
void ImGui::SeparatorText(const char* label)
1680
{
1681
ImGuiWindow* window = GetCurrentWindow();
1682
if (window->SkipItems)
1683
return;
1684
1685
// The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want:
1686
// - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight)
1687
// - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string)
1688
// - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...'
1689
// Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item,
1690
// and then we can turn this into a format function.
1691
SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f);
1692
}
1693
1694
// 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.
1695
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)
1696
{
1697
ImGuiContext& g = *GImGui;
1698
ImGuiWindow* window = g.CurrentWindow;
1699
1700
if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav))
1701
return false;
1702
1703
// FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is
1704
// to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
1705
// Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
1706
ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren;
1707
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
1708
button_flags |= ImGuiButtonFlags_AllowOverlap;
1709
#endif
1710
1711
bool hovered, held;
1712
ImRect bb_interact = bb;
1713
bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f));
1714
ButtonBehavior(bb_interact, id, &hovered, &held, button_flags);
1715
if (hovered)
1716
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb
1717
1718
if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay))
1719
SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW);
1720
1721
ImRect bb_render = bb;
1722
if (held)
1723
{
1724
float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis];
1725
1726
// Minimum pane size
1727
float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1);
1728
float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2);
1729
if (mouse_delta < -size_1_maximum_delta)
1730
mouse_delta = -size_1_maximum_delta;
1731
if (mouse_delta > size_2_maximum_delta)
1732
mouse_delta = size_2_maximum_delta;
1733
1734
// Apply resize
1735
if (mouse_delta != 0.0f)
1736
{
1737
*size1 = ImMax(*size1 + mouse_delta, min_size1);
1738
*size2 = ImMax(*size2 - mouse_delta, min_size2);
1739
bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta));
1740
MarkItemEdited(id);
1741
}
1742
}
1743
1744
// Render at new position
1745
if (bg_col & IM_COL32_A_MASK)
1746
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f);
1747
const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator);
1748
window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f);
1749
1750
return held;
1751
}
1752
1753
static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs)
1754
{
1755
const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs;
1756
const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs;
1757
if (int d = (int)(b->Width - a->Width))
1758
return d;
1759
return (b->Index - a->Index);
1760
}
1761
1762
// Shrink excess width from a set of item, by removing width from the larger items first.
1763
// Set items Width to -1.0f to disable shrinking this item.
1764
void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess)
1765
{
1766
if (count == 1)
1767
{
1768
if (items[0].Width >= 0.0f)
1769
items[0].Width = ImMax(items[0].Width - width_excess, 1.0f);
1770
return;
1771
}
1772
ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer);
1773
int count_same_width = 1;
1774
while (width_excess > 0.0f && count_same_width < count)
1775
{
1776
while (count_same_width < count && items[0].Width <= items[count_same_width].Width)
1777
count_same_width++;
1778
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);
1779
if (max_width_to_remove_per_item <= 0.0f)
1780
break;
1781
float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item);
1782
for (int item_n = 0; item_n < count_same_width; item_n++)
1783
items[item_n].Width -= width_to_remove_per_item;
1784
width_excess -= width_to_remove_per_item * count_same_width;
1785
}
1786
1787
// Round width and redistribute remainder
1788
// 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.
1789
width_excess = 0.0f;
1790
for (int n = 0; n < count; n++)
1791
{
1792
float width_rounded = ImTrunc(items[n].Width);
1793
width_excess += items[n].Width - width_rounded;
1794
items[n].Width = width_rounded;
1795
}
1796
while (width_excess > 0.0f)
1797
for (int n = 0; n < count && width_excess > 0.0f; n++)
1798
{
1799
float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f);
1800
items[n].Width += width_to_add;
1801
width_excess -= width_to_add;
1802
}
1803
}
1804
1805
//-------------------------------------------------------------------------
1806
// [SECTION] Widgets: ComboBox
1807
//-------------------------------------------------------------------------
1808
// - CalcMaxPopupHeightFromItemCount() [Internal]
1809
// - BeginCombo()
1810
// - BeginComboPopup() [Internal]
1811
// - EndCombo()
1812
// - BeginComboPreview() [Internal]
1813
// - EndComboPreview() [Internal]
1814
// - Combo()
1815
//-------------------------------------------------------------------------
1816
1817
static float CalcMaxPopupHeightFromItemCount(int items_count)
1818
{
1819
ImGuiContext& g = *GImGui;
1820
if (items_count <= 0)
1821
return FLT_MAX;
1822
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
1823
}
1824
1825
bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags)
1826
{
1827
ImGuiContext& g = *GImGui;
1828
ImGuiWindow* window = GetCurrentWindow();
1829
1830
ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
1831
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
1832
if (window->SkipItems)
1833
return false;
1834
1835
const ImGuiStyle& style = g.Style;
1836
const ImGuiID id = window->GetID(label);
1837
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
1838
if (flags & ImGuiComboFlags_WidthFitPreview)
1839
IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0);
1840
1841
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
1842
const ImVec2 label_size = CalcTextSize(label, NULL, true);
1843
const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f;
1844
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth());
1845
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
1846
const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
1847
ItemSize(total_bb, style.FramePadding.y);
1848
if (!ItemAdd(total_bb, id, &bb))
1849
return false;
1850
1851
// Open on click
1852
bool hovered, held;
1853
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
1854
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
1855
bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None);
1856
if (pressed && !popup_open)
1857
{
1858
OpenPopupEx(popup_id, ImGuiPopupFlags_None);
1859
popup_open = true;
1860
}
1861
1862
// Render shape
1863
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
1864
const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
1865
RenderNavCursor(bb, id);
1866
if (!(flags & ImGuiComboFlags_NoPreview))
1867
window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
1868
if (!(flags & ImGuiComboFlags_NoArrowButton))
1869
{
1870
ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
1871
ImU32 text_col = GetColorU32(ImGuiCol_Text);
1872
window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
1873
if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
1874
RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
1875
}
1876
RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
1877
1878
// Custom preview
1879
if (flags & ImGuiComboFlags_CustomPreview)
1880
{
1881
g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
1882
IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
1883
preview_value = NULL;
1884
}
1885
1886
// Render preview and label
1887
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
1888
{
1889
if (g.LogEnabled)
1890
LogSetNextTextDecoration("{", "}");
1891
RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
1892
}
1893
if (label_size.x > 0)
1894
RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
1895
1896
if (!popup_open)
1897
return false;
1898
1899
g.NextWindowData.Flags = backup_next_window_data_flags;
1900
return BeginComboPopup(popup_id, bb, flags);
1901
}
1902
1903
bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags)
1904
{
1905
ImGuiContext& g = *GImGui;
1906
if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None))
1907
{
1908
g.NextWindowData.ClearFlags();
1909
return false;
1910
}
1911
1912
// Set popup size
1913
float w = bb.GetWidth();
1914
if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
1915
{
1916
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
1917
}
1918
else
1919
{
1920
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
1921
flags |= ImGuiComboFlags_HeightRegular;
1922
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
1923
int popup_max_height_in_items = -1;
1924
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
1925
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
1926
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
1927
ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
1928
if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
1929
constraint_min.x = w;
1930
if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
1931
constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
1932
SetNextWindowSizeConstraints(constraint_min, constraint_max);
1933
}
1934
1935
// This is essentially a specialized version of BeginPopupEx()
1936
char name[16];
1937
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth
1938
1939
// Set position given a custom constraint (peak into expected window size so we can position it)
1940
// FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
1941
// FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
1942
if (ImGuiWindow* popup_window = FindWindowByName(name))
1943
if (popup_window->WasActive)
1944
{
1945
// Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
1946
ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
1947
popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
1948
ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
1949
ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
1950
SetNextWindowPos(pos);
1951
}
1952
1953
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
1954
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
1955
PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text
1956
bool ret = Begin(name, NULL, window_flags);
1957
PopStyleVar();
1958
if (!ret)
1959
{
1960
EndPopup();
1961
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
1962
return false;
1963
}
1964
g.BeginComboDepth++;
1965
return true;
1966
}
1967
1968
void ImGui::EndCombo()
1969
{
1970
ImGuiContext& g = *GImGui;
1971
EndPopup();
1972
g.BeginComboDepth--;
1973
}
1974
1975
// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements
1976
// (Experimental, see GitHub issues: #1658, #4168)
1977
bool ImGui::BeginComboPreview()
1978
{
1979
ImGuiContext& g = *GImGui;
1980
ImGuiWindow* window = g.CurrentWindow;
1981
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
1982
1983
if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible))
1984
return false;
1985
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?
1986
if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional)
1987
return false;
1988
1989
// FIXME: This could be contained in a PushWorkRect() api
1990
preview_data->BackupCursorPos = window->DC.CursorPos;
1991
preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos;
1992
preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine;
1993
preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset;
1994
preview_data->BackupLayout = window->DC.LayoutType;
1995
window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding;
1996
window->DC.CursorMaxPos = window->DC.CursorPos;
1997
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
1998
window->DC.IsSameLine = false;
1999
PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true);
2000
2001
return true;
2002
}
2003
2004
void ImGui::EndComboPreview()
2005
{
2006
ImGuiContext& g = *GImGui;
2007
ImGuiWindow* window = g.CurrentWindow;
2008
ImGuiComboPreviewData* preview_data = &g.ComboPreviewData;
2009
2010
// FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future
2011
ImDrawList* draw_list = window->DrawList;
2012
if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y)
2013
if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command
2014
{
2015
draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect;
2016
draw_list->_TryMergeDrawCmds();
2017
}
2018
PopClipRect();
2019
window->DC.CursorPos = preview_data->BackupCursorPos;
2020
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos);
2021
window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine;
2022
window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset;
2023
window->DC.LayoutType = preview_data->BackupLayout;
2024
window->DC.IsSameLine = false;
2025
preview_data->PreviewRect = ImRect();
2026
}
2027
2028
// Getter for the old Combo() API: const char*[]
2029
static const char* Items_ArrayGetter(void* data, int idx)
2030
{
2031
const char* const* items = (const char* const*)data;
2032
return items[idx];
2033
}
2034
2035
// Getter for the old Combo() API: "item1\0item2\0item3\0"
2036
static const char* Items_SingleStringGetter(void* data, int idx)
2037
{
2038
const char* items_separated_by_zeros = (const char*)data;
2039
int items_count = 0;
2040
const char* p = items_separated_by_zeros;
2041
while (*p)
2042
{
2043
if (idx == items_count)
2044
break;
2045
p += strlen(p) + 1;
2046
items_count++;
2047
}
2048
return *p ? p : NULL;
2049
}
2050
2051
// Old API, prefer using BeginCombo() nowadays if you can.
2052
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)
2053
{
2054
ImGuiContext& g = *GImGui;
2055
2056
// Call the getter to obtain the preview string which is a parameter to BeginCombo()
2057
const char* preview_value = NULL;
2058
if (*current_item >= 0 && *current_item < items_count)
2059
preview_value = getter(user_data, *current_item);
2060
2061
// 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.
2062
if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
2063
SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
2064
2065
if (!BeginCombo(label, preview_value, ImGuiComboFlags_None))
2066
return false;
2067
2068
// Display items
2069
bool value_changed = false;
2070
ImGuiListClipper clipper;
2071
clipper.Begin(items_count);
2072
clipper.IncludeItemByIndex(*current_item);
2073
while (clipper.Step())
2074
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
2075
{
2076
const char* item_text = getter(user_data, i);
2077
if (item_text == NULL)
2078
item_text = "*Unknown item*";
2079
2080
PushID(i);
2081
const bool item_selected = (i == *current_item);
2082
if (Selectable(item_text, item_selected) && *current_item != i)
2083
{
2084
value_changed = true;
2085
*current_item = i;
2086
}
2087
if (item_selected)
2088
SetItemDefaultFocus();
2089
PopID();
2090
}
2091
2092
EndCombo();
2093
if (value_changed)
2094
MarkItemEdited(g.LastItemData.ID);
2095
2096
return value_changed;
2097
}
2098
2099
// Combo box helper allowing to pass an array of strings.
2100
bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items)
2101
{
2102
const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items);
2103
return value_changed;
2104
}
2105
2106
// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0"
2107
bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items)
2108
{
2109
int items_count = 0;
2110
const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open
2111
while (*p)
2112
{
2113
p += strlen(p) + 1;
2114
items_count++;
2115
}
2116
bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items);
2117
return value_changed;
2118
}
2119
2120
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
2121
2122
struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); };
2123
static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx)
2124
{
2125
ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data;
2126
const char* s = NULL;
2127
data->OldCallback(data->UserData, idx, &s);
2128
return s;
2129
}
2130
2131
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)
2132
{
2133
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2134
return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items);
2135
}
2136
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)
2137
{
2138
ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter };
2139
return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items);
2140
}
2141
2142
#endif
2143
2144
//-------------------------------------------------------------------------
2145
// [SECTION] Data Type and Data Formatting Helpers [Internal]
2146
//-------------------------------------------------------------------------
2147
// - DataTypeGetInfo()
2148
// - DataTypeFormatString()
2149
// - DataTypeApplyOp()
2150
// - DataTypeApplyFromText()
2151
// - DataTypeCompare()
2152
// - DataTypeClamp()
2153
// - GetMinimumStepAtDecimalPrecision
2154
// - RoundScalarWithFormat<>()
2155
//-------------------------------------------------------------------------
2156
2157
static const ImGuiDataTypeInfo GDataTypeInfo[] =
2158
{
2159
{ sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8
2160
{ sizeof(unsigned char), "U8", "%u", "%u" },
2161
{ sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16
2162
{ sizeof(unsigned short), "U16", "%u", "%u" },
2163
{ sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32
2164
{ sizeof(unsigned int), "U32", "%u", "%u" },
2165
#ifdef _MSC_VER
2166
{ sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64
2167
{ sizeof(ImU64), "U64", "%I64u","%I64u" },
2168
#else
2169
{ sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64
2170
{ sizeof(ImU64), "U64", "%llu", "%llu" },
2171
#endif
2172
{ sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg)
2173
{ sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double
2174
{ sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool
2175
};
2176
IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT);
2177
2178
const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type)
2179
{
2180
IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT);
2181
return &GDataTypeInfo[data_type];
2182
}
2183
2184
int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format)
2185
{
2186
// Signedness doesn't matter when pushing integer arguments
2187
if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32)
2188
return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data);
2189
if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64)
2190
return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data);
2191
if (data_type == ImGuiDataType_Float)
2192
return ImFormatString(buf, buf_size, format, *(const float*)p_data);
2193
if (data_type == ImGuiDataType_Double)
2194
return ImFormatString(buf, buf_size, format, *(const double*)p_data);
2195
if (data_type == ImGuiDataType_S8)
2196
return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data);
2197
if (data_type == ImGuiDataType_U8)
2198
return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data);
2199
if (data_type == ImGuiDataType_S16)
2200
return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data);
2201
if (data_type == ImGuiDataType_U16)
2202
return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data);
2203
IM_ASSERT(0);
2204
return 0;
2205
}
2206
2207
void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2)
2208
{
2209
IM_ASSERT(op == '+' || op == '-');
2210
switch (data_type)
2211
{
2212
case ImGuiDataType_S8:
2213
if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2214
if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); }
2215
return;
2216
case ImGuiDataType_U8:
2217
if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2218
if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); }
2219
return;
2220
case ImGuiDataType_S16:
2221
if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2222
if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); }
2223
return;
2224
case ImGuiDataType_U16:
2225
if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2226
if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); }
2227
return;
2228
case ImGuiDataType_S32:
2229
if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2230
if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); }
2231
return;
2232
case ImGuiDataType_U32:
2233
if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2234
if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); }
2235
return;
2236
case ImGuiDataType_S64:
2237
if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2238
if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); }
2239
return;
2240
case ImGuiDataType_U64:
2241
if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2242
if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); }
2243
return;
2244
case ImGuiDataType_Float:
2245
if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; }
2246
if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; }
2247
return;
2248
case ImGuiDataType_Double:
2249
if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; }
2250
if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; }
2251
return;
2252
case ImGuiDataType_COUNT: break;
2253
}
2254
IM_ASSERT(0);
2255
}
2256
2257
// User can input math operators (e.g. +100) to edit a numerical values.
2258
// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess..
2259
bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty)
2260
{
2261
// Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all.
2262
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
2263
ImGuiDataTypeStorage data_backup;
2264
memcpy(&data_backup, p_data, type_info->Size);
2265
2266
while (ImCharIsBlankA(*buf))
2267
buf++;
2268
if (!buf[0])
2269
{
2270
if (p_data_when_empty != NULL)
2271
{
2272
memcpy(p_data, p_data_when_empty, type_info->Size);
2273
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2274
}
2275
return false;
2276
}
2277
2278
// Sanitize format
2279
// - 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
2280
// - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %.
2281
char format_sanitized[32];
2282
if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double)
2283
format = type_info->ScanFmt;
2284
else
2285
format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized));
2286
2287
// Small types need a 32-bit buffer to receive the result from scanf()
2288
int v32 = 0;
2289
if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1)
2290
return false;
2291
if (type_info->Size < 4)
2292
{
2293
if (data_type == ImGuiDataType_S8)
2294
*(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX);
2295
else if (data_type == ImGuiDataType_U8)
2296
*(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX);
2297
else if (data_type == ImGuiDataType_S16)
2298
*(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX);
2299
else if (data_type == ImGuiDataType_U16)
2300
*(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX);
2301
else
2302
IM_ASSERT(0);
2303
}
2304
2305
return memcmp(&data_backup, p_data, type_info->Size) != 0;
2306
}
2307
2308
template<typename T>
2309
static int DataTypeCompareT(const T* lhs, const T* rhs)
2310
{
2311
if (*lhs < *rhs) return -1;
2312
if (*lhs > *rhs) return +1;
2313
return 0;
2314
}
2315
2316
int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2)
2317
{
2318
switch (data_type)
2319
{
2320
case ImGuiDataType_S8: return DataTypeCompareT<ImS8 >((const ImS8* )arg_1, (const ImS8* )arg_2);
2321
case ImGuiDataType_U8: return DataTypeCompareT<ImU8 >((const ImU8* )arg_1, (const ImU8* )arg_2);
2322
case ImGuiDataType_S16: return DataTypeCompareT<ImS16 >((const ImS16* )arg_1, (const ImS16* )arg_2);
2323
case ImGuiDataType_U16: return DataTypeCompareT<ImU16 >((const ImU16* )arg_1, (const ImU16* )arg_2);
2324
case ImGuiDataType_S32: return DataTypeCompareT<ImS32 >((const ImS32* )arg_1, (const ImS32* )arg_2);
2325
case ImGuiDataType_U32: return DataTypeCompareT<ImU32 >((const ImU32* )arg_1, (const ImU32* )arg_2);
2326
case ImGuiDataType_S64: return DataTypeCompareT<ImS64 >((const ImS64* )arg_1, (const ImS64* )arg_2);
2327
case ImGuiDataType_U64: return DataTypeCompareT<ImU64 >((const ImU64* )arg_1, (const ImU64* )arg_2);
2328
case ImGuiDataType_Float: return DataTypeCompareT<float >((const float* )arg_1, (const float* )arg_2);
2329
case ImGuiDataType_Double: return DataTypeCompareT<double>((const double*)arg_1, (const double*)arg_2);
2330
case ImGuiDataType_COUNT: break;
2331
}
2332
IM_ASSERT(0);
2333
return 0;
2334
}
2335
2336
template<typename T>
2337
static bool DataTypeClampT(T* v, const T* v_min, const T* v_max)
2338
{
2339
// Clamp, both sides are optional, return true if modified
2340
if (v_min && *v < *v_min) { *v = *v_min; return true; }
2341
if (v_max && *v > *v_max) { *v = *v_max; return true; }
2342
return false;
2343
}
2344
2345
bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max)
2346
{
2347
switch (data_type)
2348
{
2349
case ImGuiDataType_S8: return DataTypeClampT<ImS8 >((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max);
2350
case ImGuiDataType_U8: return DataTypeClampT<ImU8 >((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max);
2351
case ImGuiDataType_S16: return DataTypeClampT<ImS16 >((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max);
2352
case ImGuiDataType_U16: return DataTypeClampT<ImU16 >((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max);
2353
case ImGuiDataType_S32: return DataTypeClampT<ImS32 >((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max);
2354
case ImGuiDataType_U32: return DataTypeClampT<ImU32 >((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max);
2355
case ImGuiDataType_S64: return DataTypeClampT<ImS64 >((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max);
2356
case ImGuiDataType_U64: return DataTypeClampT<ImU64 >((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max);
2357
case ImGuiDataType_Float: return DataTypeClampT<float >((float* )p_data, (const float* )p_min, (const float* )p_max);
2358
case ImGuiDataType_Double: return DataTypeClampT<double>((double*)p_data, (const double*)p_min, (const double*)p_max);
2359
case ImGuiDataType_COUNT: break;
2360
}
2361
IM_ASSERT(0);
2362
return false;
2363
}
2364
2365
bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data)
2366
{
2367
ImGuiContext& g = *GImGui;
2368
return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0;
2369
}
2370
2371
static float GetMinimumStepAtDecimalPrecision(int decimal_precision)
2372
{
2373
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 };
2374
if (decimal_precision < 0)
2375
return FLT_MIN;
2376
return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision);
2377
}
2378
2379
template<typename TYPE>
2380
TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v)
2381
{
2382
IM_UNUSED(data_type);
2383
IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double);
2384
const char* fmt_start = ImParseFormatFindStart(format);
2385
if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string
2386
return v;
2387
2388
// Sanitize format
2389
char fmt_sanitized[32];
2390
ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized));
2391
fmt_start = fmt_sanitized;
2392
2393
// Format value with our rounding, and read back
2394
char v_str[64];
2395
ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v);
2396
const char* p = v_str;
2397
while (*p == ' ')
2398
p++;
2399
v = (TYPE)ImAtof(p);
2400
2401
return v;
2402
}
2403
2404
//-------------------------------------------------------------------------
2405
// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc.
2406
//-------------------------------------------------------------------------
2407
// - DragBehaviorT<>() [Internal]
2408
// - DragBehavior() [Internal]
2409
// - DragScalar()
2410
// - DragScalarN()
2411
// - DragFloat()
2412
// - DragFloat2()
2413
// - DragFloat3()
2414
// - DragFloat4()
2415
// - DragFloatRange2()
2416
// - DragInt()
2417
// - DragInt2()
2418
// - DragInt3()
2419
// - DragInt4()
2420
// - DragIntRange2()
2421
//-------------------------------------------------------------------------
2422
2423
// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls)
2424
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2425
bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags)
2426
{
2427
ImGuiContext& g = *GImGui;
2428
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2429
const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange)));
2430
const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround);
2431
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2432
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2433
2434
// Default tweak speed
2435
if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX))
2436
v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio);
2437
2438
// Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings
2439
float adjust_delta = 0.0f;
2440
if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2441
{
2442
adjust_delta = g.IO.MouseDelta[axis];
2443
if (g.IO.KeyAlt)
2444
adjust_delta *= 1.0f / 100.0f;
2445
if (g.IO.KeyShift)
2446
adjust_delta *= 10.0f;
2447
}
2448
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
2449
{
2450
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
2451
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
2452
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
2453
const float tweak_factor = tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f;
2454
adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor;
2455
v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision));
2456
}
2457
adjust_delta *= v_speed;
2458
2459
// For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter.
2460
if (axis == ImGuiAxis_Y)
2461
adjust_delta = -adjust_delta;
2462
2463
// For logarithmic use our range is effectively 0..1 so scale the delta into that range
2464
if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0
2465
adjust_delta /= (float)(v_max - v_min);
2466
2467
// Clear current value on activation
2468
// 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.
2469
const bool is_just_activated = g.ActiveIdIsJustActivated;
2470
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));
2471
if (is_just_activated || is_already_past_limits_and_pushing_outward)
2472
{
2473
g.DragCurrentAccum = 0.0f;
2474
g.DragCurrentAccumDirty = false;
2475
}
2476
else if (adjust_delta != 0.0f)
2477
{
2478
g.DragCurrentAccum += adjust_delta;
2479
g.DragCurrentAccumDirty = true;
2480
}
2481
2482
if (!g.DragCurrentAccumDirty)
2483
return false;
2484
2485
TYPE v_cur = *v;
2486
FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f;
2487
2488
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
2489
const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense)
2490
if (is_logarithmic)
2491
{
2492
// 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.
2493
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
2494
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
2495
2496
// Convert to parametric space, apply delta, convert back
2497
float v_old_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2498
float v_new_parametric = v_old_parametric + g.DragCurrentAccum;
2499
v_cur = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2500
v_old_ref_for_accum_remainder = v_old_parametric;
2501
}
2502
else
2503
{
2504
v_cur += (SIGNEDTYPE)g.DragCurrentAccum;
2505
}
2506
2507
// Round to user desired precision based on format string
2508
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
2509
v_cur = RoundScalarWithFormatT<TYPE>(format, data_type, v_cur);
2510
2511
// Preserve remainder after rounding has been applied. This also allow slow tweaking of values.
2512
g.DragCurrentAccumDirty = false;
2513
if (is_logarithmic)
2514
{
2515
// Convert to parametric space, apply delta, convert back
2516
float v_new_parametric = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
2517
g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder);
2518
}
2519
else
2520
{
2521
g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v);
2522
}
2523
2524
// Lose zero sign for float/double
2525
if (v_cur == (TYPE)-0)
2526
v_cur = (TYPE)0;
2527
2528
if (*v != v_cur && is_bounded)
2529
{
2530
if (is_wrapped)
2531
{
2532
// Wrap values
2533
if (v_cur < v_min)
2534
v_cur += v_max - v_min + (is_floating_point ? 0 : 1);
2535
if (v_cur > v_max)
2536
v_cur -= v_max - v_min + (is_floating_point ? 0 : 1);
2537
}
2538
else
2539
{
2540
// Clamp values + handle overflow/wrap-around for integer types.
2541
if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point))
2542
v_cur = v_min;
2543
if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point))
2544
v_cur = v_max;
2545
}
2546
}
2547
2548
// Apply result
2549
if (*v == v_cur)
2550
return false;
2551
*v = v_cur;
2552
return true;
2553
}
2554
2555
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)
2556
{
2557
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
2558
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.");
2559
2560
ImGuiContext& g = *GImGui;
2561
if (g.ActiveId == id)
2562
{
2563
// Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation.
2564
if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0])
2565
ClearActiveID();
2566
else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
2567
ClearActiveID();
2568
}
2569
if (g.ActiveId != id)
2570
return false;
2571
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
2572
return false;
2573
2574
switch (data_type)
2575
{
2576
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; }
2577
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; }
2578
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; }
2579
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; }
2580
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);
2581
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);
2582
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);
2583
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);
2584
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);
2585
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);
2586
case ImGuiDataType_COUNT: break;
2587
}
2588
IM_ASSERT(0);
2589
return false;
2590
}
2591
2592
// 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.
2593
// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
2594
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)
2595
{
2596
ImGuiWindow* window = GetCurrentWindow();
2597
if (window->SkipItems)
2598
return false;
2599
2600
ImGuiContext& g = *GImGui;
2601
const ImGuiStyle& style = g.Style;
2602
const ImGuiID id = window->GetID(label);
2603
const float w = CalcItemWidth();
2604
2605
const ImVec2 label_size = CalcTextSize(label, NULL, true);
2606
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
2607
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));
2608
2609
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
2610
ItemSize(total_bb, style.FramePadding.y);
2611
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
2612
return false;
2613
2614
// Default format string when passing NULL
2615
if (format == NULL)
2616
format = DataTypeGetInfo(data_type)->PrintFmt;
2617
2618
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
2619
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
2620
if (!temp_input_is_active)
2621
{
2622
// Tabbing or CTRL-clicking on Drag turns it into an InputText
2623
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
2624
const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id));
2625
const bool make_active = (clicked || double_clicked || g.NavActivateId == id);
2626
if (make_active && (clicked || double_clicked))
2627
SetKeyOwner(ImGuiKey_MouseLeft, id);
2628
if (make_active && temp_input_allowed)
2629
if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
2630
temp_input_is_active = true;
2631
2632
// (Optional) simple click (without moving) turns Drag into an InputText
2633
if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active)
2634
if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR))
2635
{
2636
g.NavActivateId = id;
2637
g.NavActivateFlags = ImGuiActivateFlags_PreferInput;
2638
temp_input_is_active = true;
2639
}
2640
2641
if (make_active && !temp_input_is_active)
2642
{
2643
SetActiveID(id, window);
2644
SetFocusID(id, window);
2645
FocusWindow(window);
2646
g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
2647
}
2648
}
2649
2650
if (temp_input_is_active)
2651
{
2652
// Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
2653
bool clamp_enabled = false;
2654
if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL))
2655
{
2656
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
2657
if (p_min == NULL || p_max == NULL || clamp_range_dir < 0)
2658
clamp_enabled = true;
2659
else if (clamp_range_dir == 0)
2660
clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true;
2661
}
2662
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
2663
}
2664
2665
// Draw frame
2666
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
2667
RenderNavCursor(frame_bb, id);
2668
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding);
2669
2670
// Drag behavior
2671
const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags);
2672
if (value_changed)
2673
MarkItemEdited(id);
2674
2675
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
2676
char value_buf[64];
2677
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
2678
if (g.LogEnabled)
2679
LogSetNextTextDecoration("{", "}");
2680
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
2681
2682
if (label_size.x > 0.0f)
2683
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
2684
2685
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
2686
return value_changed;
2687
}
2688
2689
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)
2690
{
2691
ImGuiWindow* window = GetCurrentWindow();
2692
if (window->SkipItems)
2693
return false;
2694
2695
ImGuiContext& g = *GImGui;
2696
bool value_changed = false;
2697
BeginGroup();
2698
PushID(label);
2699
PushMultiItemsWidths(components, CalcItemWidth());
2700
size_t type_size = GDataTypeInfo[data_type].Size;
2701
for (int i = 0; i < components; i++)
2702
{
2703
PushID(i);
2704
if (i > 0)
2705
SameLine(0, g.Style.ItemInnerSpacing.x);
2706
value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags);
2707
PopID();
2708
PopItemWidth();
2709
p_data = (void*)((char*)p_data + type_size);
2710
}
2711
PopID();
2712
2713
const char* label_end = FindRenderedTextEnd(label);
2714
if (label != label_end)
2715
{
2716
SameLine(0, g.Style.ItemInnerSpacing.x);
2717
TextEx(label, label_end);
2718
}
2719
2720
EndGroup();
2721
return value_changed;
2722
}
2723
2724
bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2725
{
2726
return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags);
2727
}
2728
2729
bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2730
{
2731
return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags);
2732
}
2733
2734
bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2735
{
2736
return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags);
2737
}
2738
2739
bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
2740
{
2741
return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags);
2742
}
2743
2744
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2745
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)
2746
{
2747
ImGuiWindow* window = GetCurrentWindow();
2748
if (window->SkipItems)
2749
return false;
2750
2751
ImGuiContext& g = *GImGui;
2752
PushID(label);
2753
BeginGroup();
2754
PushMultiItemsWidths(2, CalcItemWidth());
2755
2756
float min_min = (v_min >= v_max) ? -FLT_MAX : v_min;
2757
float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2758
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2759
bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags);
2760
PopItemWidth();
2761
SameLine(0, g.Style.ItemInnerSpacing.x);
2762
2763
float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2764
float max_max = (v_min >= v_max) ? FLT_MAX : v_max;
2765
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2766
value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags);
2767
PopItemWidth();
2768
SameLine(0, g.Style.ItemInnerSpacing.x);
2769
2770
TextEx(label, FindRenderedTextEnd(label));
2771
EndGroup();
2772
PopID();
2773
2774
return value_changed;
2775
}
2776
2777
// NB: v_speed is float to allow adjusting the drag speed with more precision
2778
bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2779
{
2780
return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags);
2781
}
2782
2783
bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2784
{
2785
return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags);
2786
}
2787
2788
bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2789
{
2790
return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags);
2791
}
2792
2793
bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
2794
{
2795
return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags);
2796
}
2797
2798
// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this.
2799
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)
2800
{
2801
ImGuiWindow* window = GetCurrentWindow();
2802
if (window->SkipItems)
2803
return false;
2804
2805
ImGuiContext& g = *GImGui;
2806
PushID(label);
2807
BeginGroup();
2808
PushMultiItemsWidths(2, CalcItemWidth());
2809
2810
int min_min = (v_min >= v_max) ? INT_MIN : v_min;
2811
int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max);
2812
ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0);
2813
bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags);
2814
PopItemWidth();
2815
SameLine(0, g.Style.ItemInnerSpacing.x);
2816
2817
int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min);
2818
int max_max = (v_min >= v_max) ? INT_MAX : v_max;
2819
ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0);
2820
value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags);
2821
PopItemWidth();
2822
SameLine(0, g.Style.ItemInnerSpacing.x);
2823
2824
TextEx(label, FindRenderedTextEnd(label));
2825
EndGroup();
2826
PopID();
2827
2828
return value_changed;
2829
}
2830
2831
//-------------------------------------------------------------------------
2832
// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc.
2833
//-------------------------------------------------------------------------
2834
// - ScaleRatioFromValueT<> [Internal]
2835
// - ScaleValueFromRatioT<> [Internal]
2836
// - SliderBehaviorT<>() [Internal]
2837
// - SliderBehavior() [Internal]
2838
// - SliderScalar()
2839
// - SliderScalarN()
2840
// - SliderFloat()
2841
// - SliderFloat2()
2842
// - SliderFloat3()
2843
// - SliderFloat4()
2844
// - SliderAngle()
2845
// - SliderInt()
2846
// - SliderInt2()
2847
// - SliderInt3()
2848
// - SliderInt4()
2849
// - VSliderScalar()
2850
// - VSliderFloat()
2851
// - VSliderInt()
2852
//-------------------------------------------------------------------------
2853
2854
// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT)
2855
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2856
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)
2857
{
2858
if (v_min == v_max)
2859
return 0.0f;
2860
IM_UNUSED(data_type);
2861
2862
const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min);
2863
if (is_logarithmic)
2864
{
2865
bool flipped = v_max < v_min;
2866
2867
if (flipped) // Handle the case where the range is backwards
2868
ImSwap(v_min, v_max);
2869
2870
// Fudge min/max to avoid getting close to log(0)
2871
FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2872
FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2873
2874
// Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2875
if ((v_min == 0.0f) && (v_max < 0.0f))
2876
v_min_fudged = -logarithmic_zero_epsilon;
2877
else if ((v_max == 0.0f) && (v_min < 0.0f))
2878
v_max_fudged = -logarithmic_zero_epsilon;
2879
2880
float result;
2881
if (v_clamped <= v_min_fudged)
2882
result = 0.0f; // Workaround for values that are in-range but below our fudge
2883
else if (v_clamped >= v_max_fudged)
2884
result = 1.0f; // Workaround for values that are in-range but above our fudge
2885
else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions
2886
{
2887
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)
2888
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2889
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2890
if (v == 0.0f)
2891
result = zero_point_center; // Special case for exactly zero
2892
else if (v < 0.0f)
2893
result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L;
2894
else
2895
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));
2896
}
2897
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2898
result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged));
2899
else
2900
result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged));
2901
2902
return flipped ? (1.0f - result) : result;
2903
}
2904
else
2905
{
2906
// Linear slider
2907
return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min));
2908
}
2909
}
2910
2911
// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT)
2912
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2913
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)
2914
{
2915
// We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct"
2916
// but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler.
2917
if (t <= 0.0f || v_min == v_max)
2918
return v_min;
2919
if (t >= 1.0f)
2920
return v_max;
2921
2922
TYPE result = (TYPE)0;
2923
if (is_logarithmic)
2924
{
2925
// Fudge min/max to avoid getting silly results close to zero
2926
FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min;
2927
FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max;
2928
2929
const bool flipped = v_max < v_min; // Check if range is "backwards"
2930
if (flipped)
2931
ImSwap(v_min_fudged, v_max_fudged);
2932
2933
// Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon)
2934
if ((v_max == 0.0f) && (v_min < 0.0f))
2935
v_max_fudged = -logarithmic_zero_epsilon;
2936
2937
float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range
2938
2939
if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts
2940
{
2941
float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space
2942
float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize;
2943
float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize;
2944
if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R)
2945
result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise)
2946
else if (t_with_flip < zero_point_center)
2947
result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L))));
2948
else
2949
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))));
2950
}
2951
else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider
2952
result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip)));
2953
else
2954
result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip));
2955
}
2956
else
2957
{
2958
// Linear slider
2959
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2960
if (is_floating_point)
2961
{
2962
result = ImLerp(v_min, v_max, t);
2963
}
2964
else if (t < 1.0)
2965
{
2966
// - For integer values we want the clicking position to match the grab box so we round above
2967
// This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property..
2968
// - 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
2969
// range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits.
2970
FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t;
2971
result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5)));
2972
}
2973
}
2974
2975
return result;
2976
}
2977
2978
// FIXME: Try to move more of the code into shared SliderBehavior()
2979
template<typename TYPE, typename SIGNEDTYPE, typename FLOATTYPE>
2980
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)
2981
{
2982
ImGuiContext& g = *GImGui;
2983
const ImGuiStyle& style = g.Style;
2984
2985
const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X;
2986
const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0;
2987
const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double);
2988
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.
2989
2990
// Calculate bounds
2991
const float grab_padding = 2.0f; // FIXME: Should be part of style.
2992
const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f;
2993
float grab_sz = style.GrabMinSize;
2994
if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows
2995
grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit
2996
grab_sz = ImMin(grab_sz, slider_sz);
2997
const float slider_usable_sz = slider_sz - grab_sz;
2998
const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f;
2999
const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f;
3000
3001
float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true
3002
float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true
3003
if (is_logarithmic)
3004
{
3005
// 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.
3006
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1;
3007
logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision);
3008
zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f);
3009
}
3010
3011
// Process interacting with the slider
3012
bool value_changed = false;
3013
if (g.ActiveId == id)
3014
{
3015
bool set_new_value = false;
3016
float clicked_t = 0.0f;
3017
if (g.ActiveIdSource == ImGuiInputSource_Mouse)
3018
{
3019
if (!g.IO.MouseDown[0])
3020
{
3021
ClearActiveID();
3022
}
3023
else
3024
{
3025
const float mouse_abs_pos = g.IO.MousePos[axis];
3026
if (g.ActiveIdIsJustActivated)
3027
{
3028
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3029
if (axis == ImGuiAxis_Y)
3030
grab_t = 1.0f - grab_t;
3031
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3032
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.
3033
g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f;
3034
}
3035
if (slider_usable_sz > 0.0f)
3036
clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz);
3037
if (axis == ImGuiAxis_Y)
3038
clicked_t = 1.0f - clicked_t;
3039
set_new_value = true;
3040
}
3041
}
3042
else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad)
3043
{
3044
if (g.ActiveIdIsJustActivated)
3045
{
3046
g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation
3047
g.SliderCurrentAccumDirty = false;
3048
}
3049
3050
float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis);
3051
if (input_delta != 0.0f)
3052
{
3053
const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow);
3054
const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast);
3055
const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0;
3056
if (decimal_precision > 0)
3057
{
3058
input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds
3059
if (tweak_slow)
3060
input_delta /= 10.0f;
3061
}
3062
else
3063
{
3064
if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow)
3065
input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps
3066
else
3067
input_delta /= 100.0f;
3068
}
3069
if (tweak_fast)
3070
input_delta *= 10.0f;
3071
3072
g.SliderCurrentAccum += input_delta;
3073
g.SliderCurrentAccumDirty = true;
3074
}
3075
3076
float delta = g.SliderCurrentAccum;
3077
if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated)
3078
{
3079
ClearActiveID();
3080
}
3081
else if (g.SliderCurrentAccumDirty)
3082
{
3083
clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3084
3085
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
3086
{
3087
set_new_value = false;
3088
g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate
3089
}
3090
else
3091
{
3092
set_new_value = true;
3093
float old_clicked_t = clicked_t;
3094
clicked_t = ImSaturate(clicked_t + delta);
3095
3096
// Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator
3097
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3098
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3099
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3100
float new_clicked_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3101
3102
if (delta > 0)
3103
g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta);
3104
else
3105
g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta);
3106
}
3107
3108
g.SliderCurrentAccumDirty = false;
3109
}
3110
}
3111
3112
if (set_new_value)
3113
if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly))
3114
set_new_value = false;
3115
3116
if (set_new_value)
3117
{
3118
TYPE v_new = ScaleValueFromRatioT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3119
3120
// Round to user desired precision based on format string
3121
if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat))
3122
v_new = RoundScalarWithFormatT<TYPE>(format, data_type, v_new);
3123
3124
// Apply result
3125
if (*v != v_new)
3126
{
3127
*v = v_new;
3128
value_changed = true;
3129
}
3130
}
3131
}
3132
3133
if (slider_sz < 1.0f)
3134
{
3135
*out_grab_bb = ImRect(bb.Min, bb.Min);
3136
}
3137
else
3138
{
3139
// Output grab position so it can be displayed by the caller
3140
float grab_t = ScaleRatioFromValueT<TYPE, SIGNEDTYPE, FLOATTYPE>(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize);
3141
if (axis == ImGuiAxis_Y)
3142
grab_t = 1.0f - grab_t;
3143
const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t);
3144
if (axis == ImGuiAxis_X)
3145
*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);
3146
else
3147
*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);
3148
}
3149
3150
return value_changed;
3151
}
3152
3153
// For 32-bit and larger types, slider bounds are limited to half the natural type range.
3154
// 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.
3155
// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders.
3156
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)
3157
{
3158
// Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert.
3159
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.");
3160
IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX()
3161
3162
switch (data_type)
3163
{
3164
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; }
3165
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; }
3166
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; }
3167
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; }
3168
case ImGuiDataType_S32:
3169
IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2);
3170
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);
3171
case ImGuiDataType_U32:
3172
IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2);
3173
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);
3174
case ImGuiDataType_S64:
3175
IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2);
3176
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);
3177
case ImGuiDataType_U64:
3178
IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2);
3179
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);
3180
case ImGuiDataType_Float:
3181
IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f);
3182
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);
3183
case ImGuiDataType_Double:
3184
IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f);
3185
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);
3186
case ImGuiDataType_COUNT: break;
3187
}
3188
IM_ASSERT(0);
3189
return false;
3190
}
3191
3192
// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required.
3193
// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3194
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)
3195
{
3196
ImGuiWindow* window = GetCurrentWindow();
3197
if (window->SkipItems)
3198
return false;
3199
3200
ImGuiContext& g = *GImGui;
3201
const ImGuiStyle& style = g.Style;
3202
const ImGuiID id = window->GetID(label);
3203
const float w = CalcItemWidth();
3204
3205
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3206
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
3207
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));
3208
3209
const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0;
3210
ItemSize(total_bb, style.FramePadding.y);
3211
if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0))
3212
return false;
3213
3214
// Default format string when passing NULL
3215
if (format == NULL)
3216
format = DataTypeGetInfo(data_type)->PrintFmt;
3217
3218
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3219
bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id);
3220
if (!temp_input_is_active)
3221
{
3222
// Tabbing or CTRL-clicking on Slider turns it into an input box
3223
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3224
const bool make_active = (clicked || g.NavActivateId == id);
3225
if (make_active && clicked)
3226
SetKeyOwner(ImGuiKey_MouseLeft, id);
3227
if (make_active && temp_input_allowed)
3228
if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput)))
3229
temp_input_is_active = true;
3230
3231
if (make_active && !temp_input_is_active)
3232
{
3233
SetActiveID(id, window);
3234
SetFocusID(id, window);
3235
FocusWindow(window);
3236
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
3237
}
3238
}
3239
3240
if (temp_input_is_active)
3241
{
3242
// Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp)
3243
const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0;
3244
return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL);
3245
}
3246
3247
// Draw frame
3248
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3249
RenderNavCursor(frame_bb, id);
3250
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3251
3252
// Slider behavior
3253
ImRect grab_bb;
3254
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb);
3255
if (value_changed)
3256
MarkItemEdited(id);
3257
3258
// Render grab
3259
if (grab_bb.Max.x > grab_bb.Min.x)
3260
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3261
3262
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3263
char value_buf[64];
3264
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3265
if (g.LogEnabled)
3266
LogSetNextTextDecoration("{", "}");
3267
RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f));
3268
3269
if (label_size.x > 0.0f)
3270
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3271
3272
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0));
3273
return value_changed;
3274
}
3275
3276
// Add multiple sliders on 1 line for compact edition of multiple components
3277
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)
3278
{
3279
ImGuiWindow* window = GetCurrentWindow();
3280
if (window->SkipItems)
3281
return false;
3282
3283
ImGuiContext& g = *GImGui;
3284
bool value_changed = false;
3285
BeginGroup();
3286
PushID(label);
3287
PushMultiItemsWidths(components, CalcItemWidth());
3288
size_t type_size = GDataTypeInfo[data_type].Size;
3289
for (int i = 0; i < components; i++)
3290
{
3291
PushID(i);
3292
if (i > 0)
3293
SameLine(0, g.Style.ItemInnerSpacing.x);
3294
value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags);
3295
PopID();
3296
PopItemWidth();
3297
v = (void*)((char*)v + type_size);
3298
}
3299
PopID();
3300
3301
const char* label_end = FindRenderedTextEnd(label);
3302
if (label != label_end)
3303
{
3304
SameLine(0, g.Style.ItemInnerSpacing.x);
3305
TextEx(label, label_end);
3306
}
3307
3308
EndGroup();
3309
return value_changed;
3310
}
3311
3312
bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3313
{
3314
return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3315
}
3316
3317
bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3318
{
3319
return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags);
3320
}
3321
3322
bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3323
{
3324
return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags);
3325
}
3326
3327
bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3328
{
3329
return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags);
3330
}
3331
3332
bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags)
3333
{
3334
if (format == NULL)
3335
format = "%.0f deg";
3336
float v_deg = (*v_rad) * 360.0f / (2 * IM_PI);
3337
bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags);
3338
if (value_changed)
3339
*v_rad = v_deg * (2 * IM_PI) / 360.0f;
3340
return value_changed;
3341
}
3342
3343
bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3344
{
3345
return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3346
}
3347
3348
bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3349
{
3350
return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags);
3351
}
3352
3353
bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3354
{
3355
return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags);
3356
}
3357
3358
bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3359
{
3360
return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags);
3361
}
3362
3363
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)
3364
{
3365
ImGuiWindow* window = GetCurrentWindow();
3366
if (window->SkipItems)
3367
return false;
3368
3369
ImGuiContext& g = *GImGui;
3370
const ImGuiStyle& style = g.Style;
3371
const ImGuiID id = window->GetID(label);
3372
3373
const ImVec2 label_size = CalcTextSize(label, NULL, true);
3374
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size);
3375
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));
3376
3377
ItemSize(bb, style.FramePadding.y);
3378
if (!ItemAdd(frame_bb, id))
3379
return false;
3380
3381
// Default format string when passing NULL
3382
if (format == NULL)
3383
format = DataTypeGetInfo(data_type)->PrintFmt;
3384
3385
const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags);
3386
const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id);
3387
if (clicked || g.NavActivateId == id)
3388
{
3389
if (clicked)
3390
SetKeyOwner(ImGuiKey_MouseLeft, id);
3391
SetActiveID(id, window);
3392
SetFocusID(id, window);
3393
FocusWindow(window);
3394
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
3395
}
3396
3397
// Draw frame
3398
const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
3399
RenderNavCursor(frame_bb, id);
3400
RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding);
3401
3402
// Slider behavior
3403
ImRect grab_bb;
3404
const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb);
3405
if (value_changed)
3406
MarkItemEdited(id);
3407
3408
// Render grab
3409
if (grab_bb.Max.y > grab_bb.Min.y)
3410
window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding);
3411
3412
// Display value using user-provided display format so user can add prefix/suffix/decorations to the value.
3413
// For the vertical slider we allow centered text to overlap the frame padding
3414
char value_buf[64];
3415
const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format);
3416
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));
3417
if (label_size.x > 0.0f)
3418
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
3419
3420
return value_changed;
3421
}
3422
3423
bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags)
3424
{
3425
return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags);
3426
}
3427
3428
bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags)
3429
{
3430
return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags);
3431
}
3432
3433
//-------------------------------------------------------------------------
3434
// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc.
3435
//-------------------------------------------------------------------------
3436
// - ImParseFormatFindStart() [Internal]
3437
// - ImParseFormatFindEnd() [Internal]
3438
// - ImParseFormatTrimDecorations() [Internal]
3439
// - ImParseFormatSanitizeForPrinting() [Internal]
3440
// - ImParseFormatSanitizeForScanning() [Internal]
3441
// - ImParseFormatPrecision() [Internal]
3442
// - TempInputTextScalar() [Internal]
3443
// - InputScalar()
3444
// - InputScalarN()
3445
// - InputFloat()
3446
// - InputFloat2()
3447
// - InputFloat3()
3448
// - InputFloat4()
3449
// - InputInt()
3450
// - InputInt2()
3451
// - InputInt3()
3452
// - InputInt4()
3453
// - InputDouble()
3454
//-------------------------------------------------------------------------
3455
3456
// We don't use strchr() because our strings are usually very short and often start with '%'
3457
const char* ImParseFormatFindStart(const char* fmt)
3458
{
3459
while (char c = fmt[0])
3460
{
3461
if (c == '%' && fmt[1] != '%')
3462
return fmt;
3463
else if (c == '%')
3464
fmt++;
3465
fmt++;
3466
}
3467
return fmt;
3468
}
3469
3470
const char* ImParseFormatFindEnd(const char* fmt)
3471
{
3472
// Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format.
3473
if (fmt[0] != '%')
3474
return fmt;
3475
const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A'));
3476
const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a'));
3477
for (char c; (c = *fmt) != 0; fmt++)
3478
{
3479
if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0)
3480
return fmt + 1;
3481
if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0)
3482
return fmt + 1;
3483
}
3484
return fmt;
3485
}
3486
3487
// Extract the format out of a format string with leading or trailing decorations
3488
// fmt = "blah blah" -> return ""
3489
// fmt = "%.3f" -> return fmt
3490
// fmt = "hello %.3f" -> return fmt + 6
3491
// fmt = "%.3f hello" -> return buf written with "%.3f"
3492
const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size)
3493
{
3494
const char* fmt_start = ImParseFormatFindStart(fmt);
3495
if (fmt_start[0] != '%')
3496
return "";
3497
const char* fmt_end = ImParseFormatFindEnd(fmt_start);
3498
if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data.
3499
return fmt_start;
3500
ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size));
3501
return buf;
3502
}
3503
3504
// Sanitize format
3505
// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi
3506
// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi.
3507
void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3508
{
3509
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3510
IM_UNUSED(fmt_out_size);
3511
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3512
while (fmt_in < fmt_end)
3513
{
3514
char c = *fmt_in++;
3515
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3516
*(fmt_out++) = c;
3517
}
3518
*fmt_out = 0; // Zero-terminate
3519
}
3520
3521
// - 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"
3522
const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size)
3523
{
3524
const char* fmt_end = ImParseFormatFindEnd(fmt_in);
3525
const char* fmt_out_begin = fmt_out;
3526
IM_UNUSED(fmt_out_size);
3527
IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you!
3528
bool has_type = false;
3529
while (fmt_in < fmt_end)
3530
{
3531
char c = *fmt_in++;
3532
if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#'))
3533
continue;
3534
has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits
3535
if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '.
3536
*(fmt_out++) = c;
3537
}
3538
*fmt_out = 0; // Zero-terminate
3539
return fmt_out_begin;
3540
}
3541
3542
template<typename TYPE>
3543
static const char* ImAtoi(const char* src, TYPE* output)
3544
{
3545
int negative = 0;
3546
if (*src == '-') { negative = 1; src++; }
3547
if (*src == '+') { src++; }
3548
TYPE v = 0;
3549
while (*src >= '0' && *src <= '9')
3550
v = (v * 10) + (*src++ - '0');
3551
*output = negative ? -v : v;
3552
return src;
3553
}
3554
3555
// Parse display precision back from the display format string
3556
// 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.
3557
int ImParseFormatPrecision(const char* fmt, int default_precision)
3558
{
3559
fmt = ImParseFormatFindStart(fmt);
3560
if (fmt[0] != '%')
3561
return default_precision;
3562
fmt++;
3563
while (*fmt >= '0' && *fmt <= '9')
3564
fmt++;
3565
int precision = INT_MAX;
3566
if (*fmt == '.')
3567
{
3568
fmt = ImAtoi<int>(fmt + 1, &precision);
3569
if (precision < 0 || precision > 99)
3570
precision = default_precision;
3571
}
3572
if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation
3573
precision = -1;
3574
if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX)
3575
precision = -1;
3576
return (precision == INT_MAX) ? default_precision : precision;
3577
}
3578
3579
// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
3580
// FIXME: Facilitate using this in variety of other situations.
3581
// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but
3582
// the expected relationship between TempInputXXX functions and LastItemData is a little fishy.
3583
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
3584
{
3585
// On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id.
3586
// We clear ActiveID on the first frame to allow the InputText() taking it back.
3587
ImGuiContext& g = *GImGui;
3588
const bool init = (g.TempInputId != id);
3589
if (init)
3590
ClearActiveID();
3591
3592
g.CurrentWindow->DC.CursorPos = bb.Min;
3593
g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId;
3594
bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem);
3595
if (init)
3596
{
3597
// First frame we started displaying the InputText widget, we expect it to take the active id.
3598
IM_ASSERT(g.ActiveId == id);
3599
g.TempInputId = g.ActiveId;
3600
}
3601
return value_changed;
3602
}
3603
3604
// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set!
3605
// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility.
3606
// However this may not be ideal for all uses, as some user code may break on out of bound values.
3607
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)
3608
{
3609
// FIXME: May need to clarify display behavior if format doesn't contain %.
3610
// "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405
3611
ImGuiContext& g = *GImGui;
3612
const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type);
3613
char fmt_buf[32];
3614
char data_buf[32];
3615
format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf));
3616
if (format[0] == 0)
3617
format = type_info->PrintFmt;
3618
DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format);
3619
ImStrTrimBlanks(data_buf);
3620
3621
ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3622
g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData.
3623
bool value_changed = false;
3624
if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags))
3625
{
3626
// Backup old value
3627
size_t data_type_size = type_info->Size;
3628
ImGuiDataTypeStorage data_backup;
3629
memcpy(&data_backup, p_data, data_type_size);
3630
3631
// Apply new value (or operations) then clamp
3632
DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL);
3633
if (p_clamp_min || p_clamp_max)
3634
{
3635
if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0)
3636
ImSwap(p_clamp_min, p_clamp_max);
3637
DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max);
3638
}
3639
3640
// Only mark as edited if new value is different
3641
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3642
value_changed = memcmp(&data_backup, p_data, data_type_size) != 0;
3643
if (value_changed)
3644
MarkItemEdited(id);
3645
}
3646
return value_changed;
3647
}
3648
3649
void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data)
3650
{
3651
ImGuiContext& g = *GImGui;
3652
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal;
3653
memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size);
3654
}
3655
3656
// 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.
3657
// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly.
3658
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)
3659
{
3660
ImGuiWindow* window = GetCurrentWindow();
3661
if (window->SkipItems)
3662
return false;
3663
3664
ImGuiContext& g = *GImGui;
3665
ImGuiStyle& style = g.Style;
3666
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()!
3667
3668
if (format == NULL)
3669
format = DataTypeGetInfo(data_type)->PrintFmt;
3670
3671
void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue;
3672
3673
char buf[64];
3674
if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0)
3675
buf[0] = 0;
3676
else
3677
DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format);
3678
3679
// Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited.
3680
// We call MarkItemEdited() ourselves by comparing the actual data rather than the string.
3681
g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited;
3682
flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint;
3683
3684
bool value_changed = false;
3685
if (p_step == NULL)
3686
{
3687
if (InputText(label, buf, IM_ARRAYSIZE(buf), flags))
3688
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3689
}
3690
else
3691
{
3692
const float button_size = GetFrameHeight();
3693
3694
BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive()
3695
PushID(label);
3696
SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2));
3697
if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view
3698
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
3699
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
3700
3701
// Step buttons
3702
const ImVec2 backup_frame_padding = style.FramePadding;
3703
style.FramePadding.x = style.FramePadding.y;
3704
if (flags & ImGuiInputTextFlags_ReadOnly)
3705
BeginDisabled();
3706
PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
3707
SameLine(0, style.ItemInnerSpacing.x);
3708
if (ButtonEx("-", ImVec2(button_size, button_size)))
3709
{
3710
DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3711
value_changed = true;
3712
}
3713
SameLine(0, style.ItemInnerSpacing.x);
3714
if (ButtonEx("+", ImVec2(button_size, button_size)))
3715
{
3716
DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step);
3717
value_changed = true;
3718
}
3719
PopItemFlag();
3720
if (flags & ImGuiInputTextFlags_ReadOnly)
3721
EndDisabled();
3722
3723
const char* label_end = FindRenderedTextEnd(label);
3724
if (label != label_end)
3725
{
3726
SameLine(0, style.ItemInnerSpacing.x);
3727
TextEx(label, label_end);
3728
}
3729
style.FramePadding = backup_frame_padding;
3730
3731
PopID();
3732
EndGroup();
3733
}
3734
3735
g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited;
3736
if (value_changed)
3737
MarkItemEdited(g.LastItemData.ID);
3738
3739
return value_changed;
3740
}
3741
3742
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)
3743
{
3744
ImGuiWindow* window = GetCurrentWindow();
3745
if (window->SkipItems)
3746
return false;
3747
3748
ImGuiContext& g = *GImGui;
3749
bool value_changed = false;
3750
BeginGroup();
3751
PushID(label);
3752
PushMultiItemsWidths(components, CalcItemWidth());
3753
size_t type_size = GDataTypeInfo[data_type].Size;
3754
for (int i = 0; i < components; i++)
3755
{
3756
PushID(i);
3757
if (i > 0)
3758
SameLine(0, g.Style.ItemInnerSpacing.x);
3759
value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags);
3760
PopID();
3761
PopItemWidth();
3762
p_data = (void*)((char*)p_data + type_size);
3763
}
3764
PopID();
3765
3766
const char* label_end = FindRenderedTextEnd(label);
3767
if (label != label_end)
3768
{
3769
SameLine(0.0f, g.Style.ItemInnerSpacing.x);
3770
TextEx(label, label_end);
3771
}
3772
3773
EndGroup();
3774
return value_changed;
3775
}
3776
3777
bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags)
3778
{
3779
return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags);
3780
}
3781
3782
bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags)
3783
{
3784
return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags);
3785
}
3786
3787
bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags)
3788
{
3789
return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags);
3790
}
3791
3792
bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags)
3793
{
3794
return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags);
3795
}
3796
3797
bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags)
3798
{
3799
// 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.
3800
const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d";
3801
return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags);
3802
}
3803
3804
bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags)
3805
{
3806
return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags);
3807
}
3808
3809
bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags)
3810
{
3811
return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags);
3812
}
3813
3814
bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags)
3815
{
3816
return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags);
3817
}
3818
3819
bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags)
3820
{
3821
return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags);
3822
}
3823
3824
//-------------------------------------------------------------------------
3825
// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint
3826
//-------------------------------------------------------------------------
3827
// - imstb_textedit.h include
3828
// - InputText()
3829
// - InputTextWithHint()
3830
// - InputTextMultiline()
3831
// - InputTextGetCharInfo() [Internal]
3832
// - InputTextReindexLines() [Internal]
3833
// - InputTextReindexLinesRange() [Internal]
3834
// - InputTextEx() [Internal]
3835
// - DebugNodeInputTextState() [Internal]
3836
//-------------------------------------------------------------------------
3837
3838
namespace ImStb
3839
{
3840
#include "imstb_textedit.h"
3841
}
3842
3843
bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3844
{
3845
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline()
3846
return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3847
}
3848
3849
bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3850
{
3851
return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data);
3852
}
3853
3854
bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data)
3855
{
3856
IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint.
3857
return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data);
3858
}
3859
3860
// This is only used in the path where the multiline widget is inactivate.
3861
static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end)
3862
{
3863
int line_count = 0;
3864
const char* s = text_begin;
3865
while (true)
3866
{
3867
const char* s_eol = strchr(s, '\n');
3868
line_count++;
3869
if (s_eol == NULL)
3870
{
3871
s = s + strlen(s);
3872
break;
3873
}
3874
s = s_eol + 1;
3875
}
3876
*out_text_end = s;
3877
return line_count;
3878
}
3879
3880
// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA()
3881
static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line)
3882
{
3883
ImGuiContext& g = *ctx;
3884
ImFont* font = g.Font;
3885
const float line_height = g.FontSize;
3886
const float scale = line_height / font->FontSize;
3887
3888
ImVec2 text_size = ImVec2(0, 0);
3889
float line_width = 0.0f;
3890
3891
const char* s = text_begin;
3892
while (s < text_end)
3893
{
3894
unsigned int c = (unsigned int)*s;
3895
if (c < 0x80)
3896
s += 1;
3897
else
3898
s += ImTextCharFromUtf8(&c, s, text_end);
3899
3900
if (c == '\n')
3901
{
3902
text_size.x = ImMax(text_size.x, line_width);
3903
text_size.y += line_height;
3904
line_width = 0.0f;
3905
if (stop_on_new_line)
3906
break;
3907
continue;
3908
}
3909
if (c == '\r')
3910
continue;
3911
3912
const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale;
3913
line_width += char_width;
3914
}
3915
3916
if (text_size.x < line_width)
3917
text_size.x = line_width;
3918
3919
if (out_offset)
3920
*out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n
3921
3922
if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n
3923
text_size.y += line_height;
3924
3925
if (remaining)
3926
*remaining = s;
3927
3928
return text_size;
3929
}
3930
3931
// 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)
3932
// With our UTF-8 use of stb_textedit:
3933
// - 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.
3934
// - 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).
3935
// - ...but we don't use that feature.
3936
namespace ImStb
3937
{
3938
static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; }
3939
static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextA[idx]; }
3940
static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextA.Data + line_start_idx + char_idx, obj->TextA.Data + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; }
3941
static char STB_TEXTEDIT_NEWLINE = '\n';
3942
static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx)
3943
{
3944
const char* text = obj->TextA.Data;
3945
const char* text_remaining = NULL;
3946
const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true);
3947
r->x0 = 0.0f;
3948
r->x1 = size.x;
3949
r->baseline_y_delta = size.y;
3950
r->ymin = 0.0f;
3951
r->ymax = size.y;
3952
r->num_chars = (int)(text_remaining - (text + line_start_idx));
3953
}
3954
3955
#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL
3956
#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL
3957
3958
static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
3959
{
3960
if (idx >= obj->TextLen)
3961
return obj->TextLen + 1;
3962
unsigned int c;
3963
return idx + ImTextCharFromUtf8(&c, obj->TextA.Data + idx, obj->TextA.Data + obj->TextLen);
3964
}
3965
3966
static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx)
3967
{
3968
if (idx <= 0)
3969
return -1;
3970
const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, obj->TextA.Data + idx);
3971
return (int)(p - obj->TextA.Data);
3972
}
3973
3974
static bool ImCharIsSeparatorW(unsigned int c)
3975
{
3976
static const unsigned int separator_list[] =
3977
{
3978
',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D,
3979
'[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F,
3980
'\n', '\r',
3981
};
3982
for (unsigned int separator : separator_list)
3983
if (c == separator)
3984
return true;
3985
return false;
3986
}
3987
3988
static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx)
3989
{
3990
// 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.
3991
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
3992
return 0;
3993
3994
const char* curr_p = obj->TextA.Data + idx;
3995
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, curr_p);
3996
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextA.Data + obj->TextLen);
3997
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextA.Data + obj->TextLen);
3998
3999
bool prev_white = ImCharIsBlankW(prev_c);
4000
bool prev_separ = ImCharIsSeparatorW(prev_c);
4001
bool curr_white = ImCharIsBlankW(curr_c);
4002
bool curr_separ = ImCharIsSeparatorW(curr_c);
4003
return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4004
}
4005
static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx)
4006
{
4007
if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0)
4008
return 0;
4009
4010
const char* curr_p = obj->TextA.Data + idx;
4011
const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextA.Data, curr_p);
4012
unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextA.Data + obj->TextLen);
4013
unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextA.Data + obj->TextLen);
4014
4015
bool prev_white = ImCharIsBlankW(prev_c);
4016
bool prev_separ = ImCharIsSeparatorW(prev_c);
4017
bool curr_white = ImCharIsBlankW(curr_c);
4018
bool curr_separ = ImCharIsSeparatorW(curr_c);
4019
return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ);
4020
}
4021
static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx)
4022
{
4023
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4024
while (idx >= 0 && !is_word_boundary_from_right(obj, idx))
4025
idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx);
4026
return idx < 0 ? 0 : idx;
4027
}
4028
static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx)
4029
{
4030
int len = obj->TextLen;
4031
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4032
while (idx < len && !is_word_boundary_from_left(obj, idx))
4033
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4034
return idx > len ? len : idx;
4035
}
4036
static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx)
4037
{
4038
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4039
int len = obj->TextLen;
4040
while (idx < len && !is_word_boundary_from_right(obj, idx))
4041
idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx);
4042
return idx > len ? len : idx;
4043
}
4044
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); }
4045
#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h
4046
#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL
4047
4048
static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n)
4049
{
4050
char* dst = obj->TextA.Data + pos;
4051
4052
obj->Edited = true;
4053
obj->TextLen -= n;
4054
4055
// Offset remaining text (FIXME-OPT: Use memmove)
4056
const char* src = obj->TextA.Data + pos + n;
4057
while (char c = *src++)
4058
*dst++ = c;
4059
*dst = '\0';
4060
}
4061
4062
static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len)
4063
{
4064
const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4065
const int text_len = obj->TextLen;
4066
IM_ASSERT(pos <= text_len);
4067
4068
if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity))
4069
return false;
4070
4071
// Grow internal buffer if needed
4072
if (new_text_len + text_len + 1 > obj->TextA.Size)
4073
{
4074
if (!is_resizable)
4075
return false;
4076
obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1);
4077
}
4078
4079
char* text = obj->TextA.Data;
4080
if (pos != text_len)
4081
memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos));
4082
memcpy(text + pos, new_text, (size_t)new_text_len);
4083
4084
obj->Edited = true;
4085
obj->TextLen += new_text_len;
4086
obj->TextA[obj->TextLen] = '\0';
4087
4088
return true;
4089
}
4090
4091
// 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)
4092
#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left
4093
#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right
4094
#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up
4095
#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down
4096
#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line
4097
#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line
4098
#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text
4099
#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text
4100
#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor
4101
#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor
4102
#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo
4103
#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo
4104
#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word
4105
#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word
4106
#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page
4107
#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page
4108
#define STB_TEXTEDIT_K_SHIFT 0x400000
4109
4110
#define IMSTB_TEXTEDIT_IMPLEMENTATION
4111
#define IMSTB_TEXTEDIT_memmove memmove
4112
#include "imstb_textedit.h"
4113
4114
// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling
4115
// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?)
4116
static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len)
4117
{
4118
stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len);
4119
ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen);
4120
state->cursor = state->select_start = state->select_end = 0;
4121
if (text_len <= 0)
4122
return;
4123
if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len))
4124
{
4125
state->cursor = state->select_start = state->select_end = text_len;
4126
state->has_preferred_x = 0;
4127
return;
4128
}
4129
IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace()
4130
}
4131
4132
} // namespace ImStb
4133
4134
// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators.
4135
ImGuiInputTextState::ImGuiInputTextState()
4136
{
4137
memset(this, 0, sizeof(*this));
4138
Stb = IM_NEW(ImStbTexteditState);
4139
memset(Stb, 0, sizeof(*Stb));
4140
}
4141
4142
ImGuiInputTextState::~ImGuiInputTextState()
4143
{
4144
IM_DELETE(Stb);
4145
}
4146
4147
void ImGuiInputTextState::OnKeyPressed(int key)
4148
{
4149
stb_textedit_key(this, Stb, key);
4150
CursorFollow = true;
4151
CursorAnimReset();
4152
}
4153
4154
void ImGuiInputTextState::OnCharPressed(unsigned int c)
4155
{
4156
// Convert the key to a UTF8 byte sequence.
4157
// The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great.
4158
char utf8[5];
4159
ImTextCharToUtf8(utf8, c);
4160
stb_textedit_text(this, Stb, utf8, (int)strlen(utf8));
4161
CursorFollow = true;
4162
CursorAnimReset();
4163
}
4164
4165
// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header.
4166
void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking
4167
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); }
4168
bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; }
4169
void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; }
4170
int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; }
4171
int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; }
4172
int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; }
4173
void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; }
4174
void ImGuiInputTextState::ReloadUserBufAndSelectAll() { ReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; }
4175
void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { ReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; }
4176
void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { ReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; }
4177
4178
ImGuiInputTextCallbackData::ImGuiInputTextCallbackData()
4179
{
4180
memset(this, 0, sizeof(*this));
4181
}
4182
4183
// Public API to manipulate UTF-8 text
4184
// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar)
4185
// FIXME: The existence of this rarely exercised code path is a bit of a nuisance.
4186
void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count)
4187
{
4188
IM_ASSERT(pos + bytes_count <= BufTextLen);
4189
char* dst = Buf + pos;
4190
const char* src = Buf + pos + bytes_count;
4191
while (char c = *src++)
4192
*dst++ = c;
4193
*dst = '\0';
4194
4195
if (CursorPos >= pos + bytes_count)
4196
CursorPos -= bytes_count;
4197
else if (CursorPos >= pos)
4198
CursorPos = pos;
4199
SelectionStart = SelectionEnd = CursorPos;
4200
BufDirty = true;
4201
BufTextLen -= bytes_count;
4202
}
4203
4204
void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end)
4205
{
4206
// Accept null ranges
4207
if (new_text == new_text_end)
4208
return;
4209
4210
// Grow internal buffer if needed
4211
const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0;
4212
const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text);
4213
if (new_text_len + BufTextLen >= BufSize)
4214
{
4215
if (!is_resizable)
4216
return;
4217
4218
// Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the mildly similar code (until we remove the U16 buffer altogether!)
4219
ImGuiContext& g = *Ctx;
4220
ImGuiInputTextState* edit_state = &g.InputTextState;
4221
IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID);
4222
IM_ASSERT(Buf == edit_state->TextA.Data);
4223
int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1;
4224
edit_state->TextA.resize(new_buf_size + 1);
4225
Buf = edit_state->TextA.Data;
4226
BufSize = edit_state->BufCapacity = new_buf_size;
4227
}
4228
4229
if (BufTextLen != pos)
4230
memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos));
4231
memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char));
4232
Buf[BufTextLen + new_text_len] = '\0';
4233
4234
if (CursorPos >= pos)
4235
CursorPos += new_text_len;
4236
SelectionStart = SelectionEnd = CursorPos;
4237
BufDirty = true;
4238
BufTextLen += new_text_len;
4239
}
4240
4241
// Return false to discard a character.
4242
static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard)
4243
{
4244
unsigned int c = *p_char;
4245
4246
// Filter non-printable (NB: isprint is unreliable! see #2467)
4247
bool apply_named_filters = true;
4248
if (c < 0x20)
4249
{
4250
bool pass = false;
4251
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)
4252
pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0;
4253
if (!pass)
4254
return false;
4255
apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted.
4256
}
4257
4258
if (input_source_is_clipboard == false)
4259
{
4260
// We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817)
4261
if (c == 127)
4262
return false;
4263
4264
// Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME)
4265
if (c >= 0xE000 && c <= 0xF8FF)
4266
return false;
4267
}
4268
4269
// Filter Unicode ranges we are not handling in this build
4270
if (c > IM_UNICODE_CODEPOINT_MAX)
4271
return false;
4272
4273
// Generic named filters
4274
if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)))
4275
{
4276
// 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 '.'.
4277
// The standard mandate that programs starts in the "C" locale where the decimal point is '.'.
4278
// 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.
4279
// Change the default decimal_point with:
4280
// ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point;
4281
// 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.
4282
ImGuiContext& g = *ctx;
4283
const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint;
4284
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))
4285
if (c == '.' || c == ',')
4286
c = c_decimal_point;
4287
4288
// Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
4289
// While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may
4290
// scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font.
4291
if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal))
4292
if (c >= 0xFF01 && c <= 0xFF5E)
4293
c = c - 0xFF01 + 0x21;
4294
4295
// Allow 0-9 . - + * /
4296
if (flags & ImGuiInputTextFlags_CharsDecimal)
4297
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/'))
4298
return false;
4299
4300
// Allow 0-9 . - + * / e E
4301
if (flags & ImGuiInputTextFlags_CharsScientific)
4302
if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E'))
4303
return false;
4304
4305
// Allow 0-9 a-F A-F
4306
if (flags & ImGuiInputTextFlags_CharsHexadecimal)
4307
if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
4308
return false;
4309
4310
// Turn a-z into A-Z
4311
if (flags & ImGuiInputTextFlags_CharsUppercase)
4312
if (c >= 'a' && c <= 'z')
4313
c += (unsigned int)('A' - 'a');
4314
4315
if (flags & ImGuiInputTextFlags_CharsNoBlank)
4316
if (ImCharIsBlankW(c))
4317
return false;
4318
4319
*p_char = c;
4320
}
4321
4322
// Custom callback filter
4323
if (flags & ImGuiInputTextFlags_CallbackCharFilter)
4324
{
4325
ImGuiContext& g = *GImGui;
4326
ImGuiInputTextCallbackData callback_data;
4327
callback_data.Ctx = &g;
4328
callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter;
4329
callback_data.EventChar = (ImWchar)c;
4330
callback_data.Flags = flags;
4331
callback_data.UserData = user_data;
4332
if (callback(&callback_data) != 0)
4333
return false;
4334
*p_char = callback_data.EventChar;
4335
if (!callback_data.EventChar)
4336
return false;
4337
}
4338
4339
return true;
4340
}
4341
4342
// Find the shortest single replacement we can make to get the new text from the old text.
4343
// Important: needs to be run before TextW is rewritten with the new characters because calling STB_TEXTEDIT_GETCHAR() at the end.
4344
// 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.
4345
static void InputTextReconcileUndoStateAfterUserCallback(ImGuiInputTextState* state, const char* new_buf_a, int new_length_a)
4346
{
4347
const char* old_buf = state->CallbackTextBackup.Data;
4348
const int old_length = state->CallbackTextBackup.Size - 1;
4349
4350
const int shorter_length = ImMin(old_length, new_length_a);
4351
int first_diff;
4352
for (first_diff = 0; first_diff < shorter_length; first_diff++)
4353
if (old_buf[first_diff] != new_buf_a[first_diff])
4354
break;
4355
if (first_diff == old_length && first_diff == new_length_a)
4356
return;
4357
4358
int old_last_diff = old_length - 1;
4359
int new_last_diff = new_length_a - 1;
4360
for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--)
4361
if (old_buf[old_last_diff] != new_buf_a[new_last_diff])
4362
break;
4363
4364
const int insert_len = new_last_diff - first_diff + 1;
4365
const int delete_len = old_last_diff - first_diff + 1;
4366
if (insert_len > 0 || delete_len > 0)
4367
if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len))
4368
for (int i = 0; i < delete_len; i++)
4369
p[i] = old_buf[first_diff + i];
4370
}
4371
4372
// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables)
4373
// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714)
4374
// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick,
4375
// but that more likely be attractive when we do have _NoLiveEdit flag available.
4376
void ImGui::InputTextDeactivateHook(ImGuiID id)
4377
{
4378
ImGuiContext& g = *GImGui;
4379
ImGuiInputTextState* state = &g.InputTextState;
4380
if (id == 0 || state->ID != id)
4381
return;
4382
g.InputTextDeactivatedState.ID = state->ID;
4383
if (state->Flags & ImGuiInputTextFlags_ReadOnly)
4384
{
4385
g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat.
4386
}
4387
else
4388
{
4389
IM_ASSERT(state->TextA.Data != 0);
4390
g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1);
4391
memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1);
4392
}
4393
}
4394
4395
// Edit a string of text
4396
// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!".
4397
// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match
4398
// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator.
4399
// - 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.
4400
// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h
4401
// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are
4402
// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188)
4403
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)
4404
{
4405
ImGuiWindow* window = GetCurrentWindow();
4406
if (window->SkipItems)
4407
return false;
4408
4409
IM_ASSERT(buf != NULL && buf_size >= 0);
4410
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys)
4411
IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key)
4412
IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming
4413
4414
ImGuiContext& g = *GImGui;
4415
ImGuiIO& io = g.IO;
4416
const ImGuiStyle& style = g.Style;
4417
4418
const bool RENDER_SELECTION_WHEN_INACTIVE = false;
4419
const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0;
4420
4421
if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar)
4422
BeginGroup();
4423
const ImGuiID id = window->GetID(label);
4424
const ImVec2 label_size = CalcTextSize(label, NULL, true);
4425
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
4426
const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y);
4427
4428
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
4429
const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size);
4430
4431
ImGuiWindow* draw_window = window;
4432
ImVec2 inner_size = frame_size;
4433
ImGuiLastItemData item_data_backup;
4434
if (is_multiline)
4435
{
4436
ImVec2 backup_pos = window->DC.CursorPos;
4437
ItemSize(total_bb, style.FramePadding.y);
4438
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4439
{
4440
EndGroup();
4441
return false;
4442
}
4443
item_data_backup = g.LastItemData;
4444
window->DC.CursorPos = backup_pos;
4445
4446
// Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping.
4447
if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput))
4448
g.NavActivateId = 0;
4449
4450
// Prevent NavActivate reactivating in BeginChild() when we are already active.
4451
const ImGuiID backup_activate_id = g.NavActivateId;
4452
if (g.ActiveId == id) // Prevent reactivation
4453
g.NavActivateId = 0;
4454
4455
// We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug.
4456
PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]);
4457
PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
4458
PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize);
4459
PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges
4460
bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove);
4461
g.NavActivateId = backup_activate_id;
4462
PopStyleVar(3);
4463
PopStyleColor();
4464
if (!child_visible)
4465
{
4466
EndChild();
4467
EndGroup();
4468
return false;
4469
}
4470
draw_window = g.CurrentWindow; // Child window
4471
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.
4472
draw_window->DC.CursorPos += style.FramePadding;
4473
inner_size.x -= draw_window->ScrollbarSizes.x;
4474
}
4475
else
4476
{
4477
// Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd)
4478
ItemSize(total_bb, style.FramePadding.y);
4479
if (!(flags & ImGuiInputTextFlags_MergedItem))
4480
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable))
4481
return false;
4482
}
4483
4484
// Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417)
4485
bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover);
4486
if (hovered)
4487
SetMouseCursor(ImGuiMouseCursor_TextInput);
4488
if (hovered && g.NavHighlightItemUnderNav)
4489
hovered = false;
4490
4491
// We are only allowed to access the state if we are already the active widget.
4492
ImGuiInputTextState* state = GetInputTextState(id);
4493
4494
if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly)
4495
flags |= ImGuiInputTextFlags_ReadOnly;
4496
const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0;
4497
const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0;
4498
const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0;
4499
const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0;
4500
if (is_resizable)
4501
IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag!
4502
4503
const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard)));
4504
4505
const bool user_clicked = hovered && io.MouseClicked[0];
4506
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4507
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y);
4508
bool clear_active_id = false;
4509
bool select_all = false;
4510
4511
float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX;
4512
4513
const bool init_reload_from_user_buf = (state != NULL && state->ReloadUserBuf);
4514
const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state.
4515
const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav);
4516
const bool init_state = (init_make_active || user_scroll_active);
4517
if ((init_state && g.ActiveId != id) || init_changed_specs || init_reload_from_user_buf)
4518
{
4519
// Access state even if we don't own it yet.
4520
state = &g.InputTextState;
4521
state->CursorAnimReset();
4522
state->ReloadUserBuf = false;
4523
4524
// 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)
4525
InputTextDeactivateHook(state->ID);
4526
4527
// From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
4528
const int buf_len = (int)strlen(buf);
4529
if (!init_reload_from_user_buf)
4530
{
4531
// Take a copy of the initial buffer value.
4532
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.
4533
memcpy(state->TextToRevertTo.Data, buf, buf_len + 1);
4534
}
4535
4536
// Preserve cursor position and undo/redo stack if we come back to same widget
4537
// FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate?
4538
bool recycle_state = (state->ID == id && !init_changed_specs && !init_reload_from_user_buf);
4539
if (recycle_state && (state->TextLen != buf_len || (strncmp(state->TextA.Data, buf, buf_len) != 0)))
4540
recycle_state = false;
4541
4542
// Start edition
4543
state->ID = id;
4544
state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string.
4545
state->TextLen = (int)strlen(buf);
4546
memcpy(state->TextA.Data, buf, state->TextLen + 1);
4547
4548
// Find initial scroll position for right alignment
4549
state->Scroll = ImVec2(0.0f, 0.0f);
4550
if (flags & ImGuiInputTextFlags_ElideLeft)
4551
state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f);
4552
4553
if (recycle_state)
4554
{
4555
// Recycle existing cursor/selection/undo stack but clamp position
4556
// Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler.
4557
state->CursorClamp();
4558
}
4559
else
4560
{
4561
stb_textedit_initialize_state(state->Stb, !is_multiline);
4562
}
4563
4564
if (init_reload_from_user_buf)
4565
{
4566
state->Stb->select_start = state->ReloadSelectionStart;
4567
state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd;
4568
state->CursorClamp();
4569
}
4570
else if (!is_multiline)
4571
{
4572
if (flags & ImGuiInputTextFlags_AutoSelectAll)
4573
select_all = true;
4574
if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState)))
4575
select_all = true;
4576
if (user_clicked && io.KeyCtrl)
4577
select_all = true;
4578
}
4579
4580
if (flags & ImGuiInputTextFlags_AlwaysOverwrite)
4581
state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863)
4582
}
4583
4584
const bool is_osx = io.ConfigMacOSXBehaviors;
4585
if (g.ActiveId != id && init_make_active)
4586
{
4587
IM_ASSERT(state && state->ID == id);
4588
SetActiveID(id, window);
4589
SetFocusID(id, window);
4590
FocusWindow(window);
4591
}
4592
if (g.ActiveId == id)
4593
{
4594
// Declare some inputs, the other are registered and polled via Shortcut() routing system.
4595
// FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combinaison into individual shortcuts.
4596
const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End };
4597
for (ImGuiKey key : always_owned_keys)
4598
SetKeyOwner(key, id);
4599
if (user_clicked)
4600
SetKeyOwner(ImGuiKey_MouseLeft, id);
4601
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right);
4602
if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory))
4603
{
4604
g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down);
4605
SetKeyOwner(ImGuiKey_UpArrow, id);
4606
SetKeyOwner(ImGuiKey_DownArrow, id);
4607
}
4608
if (is_multiline)
4609
{
4610
SetKeyOwner(ImGuiKey_PageUp, id);
4611
SetKeyOwner(ImGuiKey_PageDown, id);
4612
}
4613
// FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu
4614
if (is_osx)
4615
SetKeyOwner(ImGuiMod_Alt, id);
4616
4617
// Expose scroll in a manner that is agnostic to us using a child window
4618
if (is_multiline && state != NULL)
4619
state->Scroll.y = draw_window->Scroll.y;
4620
}
4621
4622
// 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)
4623
if (g.ActiveId == id && state == NULL)
4624
ClearActiveID();
4625
4626
// Release focus when we click outside
4627
if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560
4628
clear_active_id = true;
4629
4630
// Lock the decision of whether we are going to take the path displaying the cursor or selection
4631
bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active);
4632
bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4633
bool value_changed = false;
4634
bool validated = false;
4635
4636
// Select the buffer to render.
4637
const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state;
4638
const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0);
4639
4640
// Password pushes a temporary font with only a fallback glyph
4641
if (is_password && !is_displaying_hint)
4642
{
4643
const ImFontGlyph* glyph = g.Font->FindGlyph('*');
4644
ImFont* password_font = &g.InputTextPasswordFont;
4645
password_font->FontSize = g.Font->FontSize;
4646
password_font->Scale = g.Font->Scale;
4647
password_font->Ascent = g.Font->Ascent;
4648
password_font->Descent = g.Font->Descent;
4649
password_font->ContainerAtlas = g.Font->ContainerAtlas;
4650
password_font->FallbackGlyph = glyph;
4651
password_font->FallbackAdvanceX = glyph->AdvanceX;
4652
IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty());
4653
PushFont(password_font);
4654
}
4655
4656
// Process mouse inputs and character inputs
4657
if (g.ActiveId == id)
4658
{
4659
IM_ASSERT(state != NULL);
4660
state->Edited = false;
4661
state->BufCapacity = buf_size;
4662
state->Flags = flags;
4663
4664
// Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget.
4665
// Down the line we should have a cleaner library-wide concept of Selected vs Active.
4666
g.ActiveIdAllowOverlap = !io.MouseDown[0];
4667
4668
// Edit in progress
4669
const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x;
4670
const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f));
4671
4672
if (select_all)
4673
{
4674
state->SelectAll();
4675
state->SelectedAllMouseLock = true;
4676
}
4677
else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift)
4678
{
4679
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4680
const int multiclick_count = (io.MouseClickedCount[0] - 2);
4681
if ((multiclick_count % 2) == 0)
4682
{
4683
// Double-click: Select word
4684
// We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant:
4685
// FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS)
4686
const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n';
4687
if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol)
4688
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT);
4689
//state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4690
if (!STB_TEXT_HAS_SELECTION(state->Stb))
4691
ImStb::stb_textedit_prep_selection_at_cursor(state->Stb);
4692
state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor);
4693
state->Stb->select_end = state->Stb->cursor;
4694
ImStb::stb_textedit_clamp(state, state->Stb);
4695
}
4696
else
4697
{
4698
// Triple-click: Select line
4699
const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n';
4700
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART);
4701
state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT);
4702
state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT);
4703
if (!is_eol && is_multiline)
4704
{
4705
ImSwap(state->Stb->select_start, state->Stb->select_end);
4706
state->Stb->cursor = state->Stb->select_end;
4707
}
4708
state->CursorFollow = false;
4709
}
4710
state->CursorAnimReset();
4711
}
4712
else if (io.MouseClicked[0] && !state->SelectedAllMouseLock)
4713
{
4714
if (hovered)
4715
{
4716
if (io.KeyShift)
4717
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4718
else
4719
stb_textedit_click(state, state->Stb, mouse_x, mouse_y);
4720
state->CursorAnimReset();
4721
}
4722
}
4723
else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f))
4724
{
4725
stb_textedit_drag(state, state->Stb, mouse_x, mouse_y);
4726
state->CursorAnimReset();
4727
state->CursorFollow = true;
4728
}
4729
if (state->SelectedAllMouseLock && !io.MouseDown[0])
4730
state->SelectedAllMouseLock = false;
4731
4732
// We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336)
4733
// (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)
4734
if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly)
4735
{
4736
if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id))
4737
{
4738
unsigned int c = '\t'; // Insert TAB
4739
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4740
state->OnCharPressed(c);
4741
}
4742
// FIXME: Implement Shift+Tab
4743
/*
4744
if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id))
4745
{
4746
}
4747
*/
4748
}
4749
4750
// Process regular text input (before we check for Return because using some IME will effectively send a Return?)
4751
// 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.
4752
const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl);
4753
if (io.InputQueueCharacters.Size > 0)
4754
{
4755
if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav)
4756
for (int n = 0; n < io.InputQueueCharacters.Size; n++)
4757
{
4758
// Insert character if they pass filtering
4759
unsigned int c = (unsigned int)io.InputQueueCharacters[n];
4760
if (c == '\t') // Skip Tab, see above.
4761
continue;
4762
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4763
state->OnCharPressed(c);
4764
}
4765
4766
// Consume characters
4767
io.InputQueueCharacters.resize(0);
4768
}
4769
}
4770
4771
// Process other shortcuts/key-presses
4772
bool revert_edit = false;
4773
if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id)
4774
{
4775
IM_ASSERT(state != NULL);
4776
4777
const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1);
4778
state->Stb->row_count_per_page = row_count_per_page;
4779
4780
const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0);
4781
const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl
4782
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
4783
4784
// Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: formet would be handled by InputText)
4785
// Otherwise we could simply assume that we own the keys as we are active.
4786
const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat;
4787
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());
4788
const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection());
4789
const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly;
4790
const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable;
4791
const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || (is_osx && Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id))) && !is_readonly && is_undoable;
4792
const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id);
4793
4794
// 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.
4795
const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0;
4796
const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true);
4797
const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false));
4798
const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id));
4799
4800
// FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of.
4801
// FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line.
4802
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); }
4803
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); }
4804
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); }
4805
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); }
4806
else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; }
4807
else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; }
4808
else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); }
4809
else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); }
4810
else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut)
4811
{
4812
if (!state->HasSelection())
4813
{
4814
// 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)
4815
if (is_wordmove_key_down)
4816
state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT);
4817
}
4818
state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask);
4819
}
4820
else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly)
4821
{
4822
if (!state->HasSelection())
4823
{
4824
if (is_wordmove_key_down)
4825
state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT);
4826
else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper)
4827
state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT);
4828
}
4829
state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask);
4830
}
4831
else if (is_enter_pressed || is_gamepad_validate)
4832
{
4833
// Determine if we turn Enter into a \n character
4834
bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0;
4835
if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl))
4836
{
4837
validated = true;
4838
if (io.ConfigInputTextEnterKeepActive && !is_multiline)
4839
state->SelectAll(); // No need to scroll
4840
else
4841
clear_active_id = true;
4842
}
4843
else if (!is_readonly)
4844
{
4845
unsigned int c = '\n'; // Insert new line
4846
if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data))
4847
state->OnCharPressed(c);
4848
}
4849
}
4850
else if (is_cancel)
4851
{
4852
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4853
{
4854
if (buf[0] != 0)
4855
{
4856
revert_edit = true;
4857
}
4858
else
4859
{
4860
render_cursor = render_selection = false;
4861
clear_active_id = true;
4862
}
4863
}
4864
else
4865
{
4866
clear_active_id = revert_edit = true;
4867
render_cursor = render_selection = false;
4868
}
4869
}
4870
else if (is_undo || is_redo)
4871
{
4872
state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO);
4873
state->ClearSelection();
4874
}
4875
else if (is_select_all)
4876
{
4877
state->SelectAll();
4878
state->CursorFollow = true;
4879
}
4880
else if (is_cut || is_copy)
4881
{
4882
// Cut, Copy
4883
if (g.PlatformIO.Platform_SetClipboardTextFn != NULL)
4884
{
4885
const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0;
4886
const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen;
4887
4888
char backup = state->TextA.Data[ie];
4889
state->TextA.Data[ie] = 0; // A bit of a hack since SetClipboardText only takes null terminated strings
4890
SetClipboardText(state->TextA.Data + ib);
4891
state->TextA.Data[ie] = backup;
4892
}
4893
if (is_cut)
4894
{
4895
if (!state->HasSelection())
4896
state->SelectAll();
4897
state->CursorFollow = true;
4898
stb_textedit_cut(state, state->Stb);
4899
}
4900
}
4901
else if (is_paste)
4902
{
4903
if (const char* clipboard = GetClipboardText())
4904
{
4905
// Filter pasted buffer
4906
const int clipboard_len = (int)strlen(clipboard);
4907
char* clipboard_filtered = (char*)IM_ALLOC(clipboard_len + 1);
4908
int clipboard_filtered_len = 0;
4909
for (const char* s = clipboard; *s != 0; )
4910
{
4911
unsigned int c;
4912
int len = ImTextCharFromUtf8(&c, s, NULL);
4913
s += len;
4914
if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true))
4915
continue;
4916
memcpy(clipboard_filtered + clipboard_filtered_len, s - len, len);
4917
clipboard_filtered_len += len;
4918
}
4919
clipboard_filtered[clipboard_filtered_len] = 0;
4920
if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
4921
{
4922
stb_textedit_paste(state, state->Stb, clipboard_filtered, clipboard_filtered_len);
4923
state->CursorFollow = true;
4924
}
4925
MemFree(clipboard_filtered);
4926
}
4927
}
4928
4929
// Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame.
4930
render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor);
4931
}
4932
4933
// Process callbacks and apply result back to user's buffer.
4934
const char* apply_new_text = NULL;
4935
int apply_new_text_length = 0;
4936
if (g.ActiveId == id)
4937
{
4938
IM_ASSERT(state != NULL);
4939
if (revert_edit && !is_readonly)
4940
{
4941
if (flags & ImGuiInputTextFlags_EscapeClearsAll)
4942
{
4943
// Clear input
4944
IM_ASSERT(buf[0] != 0);
4945
apply_new_text = "";
4946
apply_new_text_length = 0;
4947
value_changed = true;
4948
IMSTB_TEXTEDIT_CHARTYPE empty_string;
4949
stb_textedit_replace(state, state->Stb, &empty_string, 0);
4950
}
4951
else if (strcmp(buf, state->TextToRevertTo.Data) != 0)
4952
{
4953
apply_new_text = state->TextToRevertTo.Data;
4954
apply_new_text_length = state->TextToRevertTo.Size - 1;
4955
4956
// Restore initial value. Only return true if restoring to the initial value changes the current buffer contents.
4957
// Push records into the undo stack so we can CTRL+Z the revert operation itself
4958
value_changed = true;
4959
stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1);
4960
}
4961
}
4962
4963
// When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer
4964
// before clearing ActiveId, even though strictly speaking it wasn't modified on this frame.
4965
// If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail.
4966
// This also allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage
4967
// (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object
4968
// unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize).
4969
const bool apply_edit_back_to_user_buffer = !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0);
4970
if (apply_edit_back_to_user_buffer)
4971
{
4972
// Apply new value immediately - copy modified buffer back
4973
// Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer
4974
// FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect.
4975
// FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks.
4976
4977
// User callback
4978
if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0)
4979
{
4980
IM_ASSERT(callback != NULL);
4981
4982
// The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment.
4983
ImGuiInputTextFlags event_flag = 0;
4984
ImGuiKey event_key = ImGuiKey_None;
4985
if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id))
4986
{
4987
event_flag = ImGuiInputTextFlags_CallbackCompletion;
4988
event_key = ImGuiKey_Tab;
4989
}
4990
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow))
4991
{
4992
event_flag = ImGuiInputTextFlags_CallbackHistory;
4993
event_key = ImGuiKey_UpArrow;
4994
}
4995
else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow))
4996
{
4997
event_flag = ImGuiInputTextFlags_CallbackHistory;
4998
event_key = ImGuiKey_DownArrow;
4999
}
5000
else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited)
5001
{
5002
event_flag = ImGuiInputTextFlags_CallbackEdit;
5003
}
5004
else if (flags & ImGuiInputTextFlags_CallbackAlways)
5005
{
5006
event_flag = ImGuiInputTextFlags_CallbackAlways;
5007
}
5008
5009
if (event_flag)
5010
{
5011
ImGuiInputTextCallbackData callback_data;
5012
callback_data.Ctx = &g;
5013
callback_data.EventFlag = event_flag;
5014
callback_data.Flags = flags;
5015
callback_data.UserData = callback_user_data;
5016
5017
// FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925
5018
state->CallbackTextBackup.resize(state->TextLen + 1);
5019
memcpy(state->CallbackTextBackup.Data, state->TextA.Data, state->TextLen + 1);
5020
5021
char* callback_buf = is_readonly ? buf : state->TextA.Data;
5022
callback_data.EventKey = event_key;
5023
callback_data.Buf = callback_buf;
5024
callback_data.BufTextLen = state->TextLen;
5025
callback_data.BufSize = state->BufCapacity;
5026
callback_data.BufDirty = false;
5027
5028
const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor;
5029
const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start;
5030
const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end;
5031
5032
// Call user code
5033
callback(&callback_data);
5034
5035
// Read back what user may have modified
5036
callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback
5037
IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields
5038
IM_ASSERT(callback_data.BufSize == state->BufCapacity);
5039
IM_ASSERT(callback_data.Flags == flags);
5040
const bool buf_dirty = callback_data.BufDirty;
5041
if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; }
5042
if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; }
5043
if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; }
5044
if (buf_dirty)
5045
{
5046
// Callback may update buffer and thus set buf_dirty even in read-only mode.
5047
IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text!
5048
InputTextReconcileUndoStateAfterUserCallback(state, callback_data.Buf, callback_data.BufTextLen); // FIXME: Move the rest of this block inside function and rename to InputTextReconcileStateAfterUserCallback() ?
5049
state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen()
5050
state->CursorAnimReset();
5051
}
5052
}
5053
}
5054
5055
// Will copy result string if modified
5056
if (!is_readonly && strcmp(state->TextA.Data, buf) != 0)
5057
{
5058
apply_new_text = state->TextA.Data;
5059
apply_new_text_length = state->TextLen;
5060
value_changed = true;
5061
}
5062
}
5063
}
5064
5065
// Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details)
5066
if (g.InputTextDeactivatedState.ID == id)
5067
{
5068
if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
5069
{
5070
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
5071
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
5072
value_changed = true;
5073
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
5074
}
5075
g.InputTextDeactivatedState.ID = 0;
5076
}
5077
5078
// Copy result to user buffer. This can currently only happen when (g.ActiveId == id)
5079
if (apply_new_text != NULL)
5080
{
5081
//// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size
5082
//// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used
5083
//// without any storage on user's side.
5084
IM_ASSERT(apply_new_text_length >= 0);
5085
if (is_resizable)
5086
{
5087
ImGuiInputTextCallbackData callback_data;
5088
callback_data.Ctx = &g;
5089
callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize;
5090
callback_data.Flags = flags;
5091
callback_data.Buf = buf;
5092
callback_data.BufTextLen = apply_new_text_length;
5093
callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1);
5094
callback_data.UserData = callback_user_data;
5095
callback(&callback_data);
5096
buf = callback_data.Buf;
5097
buf_size = callback_data.BufSize;
5098
apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1);
5099
IM_ASSERT(apply_new_text_length <= buf_size);
5100
}
5101
//IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length);
5102
5103
// If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size.
5104
ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size));
5105
}
5106
5107
// Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value)
5108
// Otherwise request text input ahead for next frame.
5109
if (g.ActiveId == id && clear_active_id)
5110
ClearActiveID();
5111
else if (g.ActiveId == id)
5112
g.WantTextInputNextFrame = 1;
5113
5114
// Render frame
5115
if (!is_multiline)
5116
{
5117
RenderNavCursor(frame_bb, id);
5118
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
5119
}
5120
5121
const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size
5122
ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding;
5123
ImVec2 text_size(0.0f, 0.0f);
5124
5125
// Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line
5126
// without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether.
5127
// Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash.
5128
const int buf_display_max_length = 2 * 1024 * 1024;
5129
const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595
5130
const char* buf_display_end = NULL; // We have specialized paths below for setting the length
5131
if (is_displaying_hint)
5132
{
5133
buf_display = hint;
5134
buf_display_end = hint + strlen(hint);
5135
}
5136
5137
// Render text. We currently only render selection when the widget is active or while scrolling.
5138
// FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive.
5139
if (render_cursor || render_selection)
5140
{
5141
IM_ASSERT(state != NULL);
5142
if (!is_displaying_hint)
5143
buf_display_end = buf_display + state->TextLen;
5144
5145
// Render text (with cursor and selection)
5146
// This is going to be messy. We need to:
5147
// - Display the text (this alone can be more easily clipped)
5148
// - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation)
5149
// - Measure text height (for scrollbar)
5150
// 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)
5151
// FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8.
5152
const char* text_begin = state->TextA.Data;
5153
const char* text_end = text_begin + state->TextLen;
5154
ImVec2 cursor_offset, select_start_offset;
5155
5156
{
5157
// Find lines numbers straddling cursor and selection min position
5158
int cursor_line_no = render_cursor ? -1 : -1000;
5159
int selmin_line_no = render_selection ? -1 : -1000;
5160
const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL;
5161
const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL;
5162
5163
// Count lines and find line number for cursor and selection ends
5164
int line_count = 1;
5165
if (is_multiline)
5166
{
5167
for (const char* s = text_begin; (s = (const char*)memchr(s, '\n', (size_t)(text_end - s))) != NULL; s++)
5168
{
5169
if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; }
5170
if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; }
5171
line_count++;
5172
}
5173
}
5174
if (cursor_line_no == -1)
5175
cursor_line_no = line_count;
5176
if (selmin_line_no == -1)
5177
selmin_line_no = line_count;
5178
5179
// Calculate 2d position by finding the beginning of the line and measuring distance
5180
cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x;
5181
cursor_offset.y = cursor_line_no * g.FontSize;
5182
if (selmin_line_no >= 0)
5183
{
5184
select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x;
5185
select_start_offset.y = selmin_line_no * g.FontSize;
5186
}
5187
5188
// Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224)
5189
if (is_multiline)
5190
text_size = ImVec2(inner_size.x, line_count * g.FontSize);
5191
}
5192
5193
// Scroll
5194
if (render_cursor && state->CursorFollow)
5195
{
5196
// Horizontal scroll in chunks of quarter width
5197
if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll))
5198
{
5199
const float scroll_increment_x = inner_size.x * 0.25f;
5200
const float visible_width = inner_size.x - style.FramePadding.x;
5201
if (cursor_offset.x < state->Scroll.x)
5202
state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x));
5203
else if (cursor_offset.x - visible_width >= state->Scroll.x)
5204
state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x);
5205
}
5206
else
5207
{
5208
state->Scroll.y = 0.0f;
5209
}
5210
5211
// Vertical scroll
5212
if (is_multiline)
5213
{
5214
// Test if cursor is vertically visible
5215
if (cursor_offset.y - g.FontSize < scroll_y)
5216
scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize);
5217
else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y)
5218
scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f;
5219
const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f);
5220
scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y);
5221
draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag
5222
draw_window->Scroll.y = scroll_y;
5223
}
5224
5225
state->CursorFollow = false;
5226
}
5227
5228
// Draw selection
5229
const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f);
5230
if (render_selection)
5231
{
5232
const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end);
5233
const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end);
5234
5235
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.
5236
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.
5237
float bg_offy_dn = is_multiline ? 0.0f : 2.0f;
5238
ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll;
5239
for (const char* p = text_selected_begin; p < text_selected_end; )
5240
{
5241
if (rect_pos.y > clip_rect.w + g.FontSize)
5242
break;
5243
if (rect_pos.y < clip_rect.y)
5244
{
5245
p = (const char*)memchr((void*)p, '\n', text_selected_end - p);
5246
p = p ? p + 1 : text_selected_end;
5247
}
5248
else
5249
{
5250
ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true);
5251
if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines
5252
ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn));
5253
rect.ClipWith(clip_rect);
5254
if (rect.Overlaps(clip_rect))
5255
draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color);
5256
rect_pos.x = draw_pos.x - draw_scroll.x;
5257
}
5258
rect_pos.y += g.FontSize;
5259
}
5260
}
5261
5262
// We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash.
5263
// FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it.
5264
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5265
{
5266
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5267
draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5268
}
5269
5270
// Draw blinking cursor
5271
if (render_cursor)
5272
{
5273
state->CursorAnim += io.DeltaTime;
5274
bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f;
5275
ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll);
5276
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);
5277
if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect))
5278
draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text));
5279
5280
// 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.)
5281
if (!is_readonly)
5282
{
5283
g.PlatformImeData.WantVisible = true;
5284
g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize);
5285
g.PlatformImeData.InputLineHeight = g.FontSize;
5286
g.PlatformImeViewport = window->Viewport->ID;
5287
}
5288
}
5289
}
5290
else
5291
{
5292
// Render text only (no selection, no cursor)
5293
if (is_multiline)
5294
text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width
5295
else if (!is_displaying_hint && g.ActiveId == id)
5296
buf_display_end = buf_display + state->TextLen;
5297
else if (!is_displaying_hint)
5298
buf_display_end = buf_display + strlen(buf_display);
5299
5300
if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length)
5301
{
5302
// Find render position for right alignment
5303
if (flags & ImGuiInputTextFlags_ElideLeft)
5304
draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x);
5305
5306
const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive?
5307
ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text);
5308
draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect);
5309
}
5310
}
5311
5312
if (is_password && !is_displaying_hint)
5313
PopFont();
5314
5315
if (is_multiline)
5316
{
5317
// For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)...
5318
Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y));
5319
g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop;
5320
EndChild();
5321
item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow);
5322
5323
// ...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...
5324
// FIXME: This quite messy/tricky, should attempt to get rid of the child window.
5325
EndGroup();
5326
if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y))
5327
{
5328
g.LastItemData.ID = id;
5329
g.LastItemData.ItemFlags = item_data_backup.ItemFlags;
5330
g.LastItemData.StatusFlags = item_data_backup.StatusFlags;
5331
}
5332
}
5333
5334
// Log as text
5335
if (g.LogEnabled && (!is_password || is_displaying_hint))
5336
{
5337
LogSetNextTextDecoration("{", "}");
5338
LogRenderedText(&draw_pos, buf_display, buf_display_end);
5339
}
5340
5341
if (label_size.x > 0)
5342
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
5343
5344
if (value_changed)
5345
MarkItemEdited(id);
5346
5347
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable);
5348
if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0)
5349
return validated;
5350
else
5351
return value_changed;
5352
}
5353
5354
void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state)
5355
{
5356
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
5357
ImGuiContext& g = *GImGui;
5358
ImStb::STB_TexteditState* stb_state = state->Stb;
5359
ImStb::StbUndoState* undo_state = &stb_state->undostate;
5360
Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId);
5361
DebugLocateItemOnHover(state->ID);
5362
Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end);
5363
Text("BufCapacityA: %d", state->BufCapacity);
5364
Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity);
5365
Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x);
5366
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);
5367
if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state
5368
{
5369
PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
5370
for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++)
5371
{
5372
ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n];
5373
const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' ';
5374
if (undo_rec_type == ' ')
5375
BeginDisabled();
5376
const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0;
5377
const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage;
5378
Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"",
5379
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);
5380
if (undo_rec_type == ' ')
5381
EndDisabled();
5382
}
5383
PopStyleVar();
5384
}
5385
EndChild();
5386
#else
5387
IM_UNUSED(state);
5388
#endif
5389
}
5390
5391
//-------------------------------------------------------------------------
5392
// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc.
5393
//-------------------------------------------------------------------------
5394
// - ColorEdit3()
5395
// - ColorEdit4()
5396
// - ColorPicker3()
5397
// - RenderColorRectWithAlphaCheckerboard() [Internal]
5398
// - ColorPicker4()
5399
// - ColorButton()
5400
// - SetColorEditOptions()
5401
// - ColorTooltip() [Internal]
5402
// - ColorEditOptionsPopup() [Internal]
5403
// - ColorPickerOptionsPopup() [Internal]
5404
//-------------------------------------------------------------------------
5405
5406
bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags)
5407
{
5408
return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha);
5409
}
5410
5411
static void ColorEditRestoreH(const float* col, float* H)
5412
{
5413
ImGuiContext& g = *GImGui;
5414
IM_ASSERT(g.ColorEditCurrentID != 0);
5415
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5416
return;
5417
*H = g.ColorEditSavedHue;
5418
}
5419
5420
// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation.
5421
// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting.
5422
static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V)
5423
{
5424
ImGuiContext& g = *GImGui;
5425
IM_ASSERT(g.ColorEditCurrentID != 0);
5426
if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)))
5427
return;
5428
5429
// When S == 0, H is undefined.
5430
// When H == 1 it wraps around to 0.
5431
if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1))
5432
*H = g.ColorEditSavedHue;
5433
5434
// When V == 0, S is undefined.
5435
if (*V == 0.0f)
5436
*S = g.ColorEditSavedSat;
5437
}
5438
5439
// Edit colors components (each component in 0.0f..1.0f range).
5440
// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5441
// 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.
5442
bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags)
5443
{
5444
ImGuiWindow* window = GetCurrentWindow();
5445
if (window->SkipItems)
5446
return false;
5447
5448
ImGuiContext& g = *GImGui;
5449
const ImGuiStyle& style = g.Style;
5450
const float square_sz = GetFrameHeight();
5451
const char* label_display_end = FindRenderedTextEnd(label);
5452
float w_full = CalcItemWidth();
5453
g.NextItemData.ClearFlags();
5454
5455
BeginGroup();
5456
PushID(label);
5457
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5458
if (set_current_color_edit_id)
5459
g.ColorEditCurrentID = window->IDStack.back();
5460
5461
// If we're not showing any slider there's no point in doing any HSV conversions
5462
const ImGuiColorEditFlags flags_untouched = flags;
5463
if (flags & ImGuiColorEditFlags_NoInputs)
5464
flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions;
5465
5466
// Context menu: display and modify options (before defaults are applied)
5467
if (!(flags & ImGuiColorEditFlags_NoOptions))
5468
ColorEditOptionsPopup(col, flags);
5469
5470
// Read stored options
5471
if (!(flags & ImGuiColorEditFlags_DisplayMask_))
5472
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_);
5473
if (!(flags & ImGuiColorEditFlags_DataTypeMask_))
5474
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_);
5475
if (!(flags & ImGuiColorEditFlags_PickerMask_))
5476
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_);
5477
if (!(flags & ImGuiColorEditFlags_InputMask_))
5478
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_);
5479
flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_));
5480
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected
5481
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5482
5483
const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0;
5484
const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0;
5485
const int components = alpha ? 4 : 3;
5486
const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x);
5487
const float w_inputs = ImMax(w_full - w_button, 1.0f);
5488
w_full = w_inputs + w_button;
5489
5490
// Convert to the formats we need
5491
float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f };
5492
if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB))
5493
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5494
else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV))
5495
{
5496
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5497
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5498
ColorEditRestoreHS(col, &f[0], &f[1], &f[2]);
5499
}
5500
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]) };
5501
5502
bool value_changed = false;
5503
bool value_changed_as_float = false;
5504
5505
const ImVec2 pos = window->DC.CursorPos;
5506
const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f;
5507
window->DC.CursorPos.x = pos.x + inputs_offset_x;
5508
5509
if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5510
{
5511
// RGB/HSV 0..255 Sliders
5512
const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1);
5513
5514
const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x);
5515
static const char* ids[4] = { "##X", "##Y", "##Z", "##W" };
5516
static const char* fmt_table_int[3][4] =
5517
{
5518
{ "%3d", "%3d", "%3d", "%3d" }, // Short display
5519
{ "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA
5520
{ "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA
5521
};
5522
static const char* fmt_table_float[3][4] =
5523
{
5524
{ "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display
5525
{ "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA
5526
{ "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA
5527
};
5528
const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1;
5529
5530
float prev_split = 0.0f;
5531
for (int n = 0; n < components; n++)
5532
{
5533
if (n > 0)
5534
SameLine(0, style.ItemInnerSpacing.x);
5535
float next_split = IM_TRUNC(w_items * (n + 1) / components);
5536
SetNextItemWidth(ImMax(next_split - prev_split, 1.0f));
5537
prev_split = next_split;
5538
5539
// FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0.
5540
if (flags & ImGuiColorEditFlags_Float)
5541
{
5542
value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]);
5543
value_changed_as_float |= value_changed;
5544
}
5545
else
5546
{
5547
value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]);
5548
}
5549
if (!(flags & ImGuiColorEditFlags_NoOptions))
5550
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5551
}
5552
}
5553
else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0)
5554
{
5555
// RGB Hexadecimal Input
5556
char buf[64];
5557
if (alpha)
5558
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255));
5559
else
5560
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255));
5561
SetNextItemWidth(w_inputs);
5562
if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase))
5563
{
5564
value_changed = true;
5565
char* p = buf;
5566
while (*p == '#' || ImCharIsBlankA(*p))
5567
p++;
5568
i[0] = i[1] = i[2] = 0;
5569
i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha)
5570
int r;
5571
if (alpha)
5572
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)
5573
else
5574
r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]);
5575
IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'.
5576
}
5577
if (!(flags & ImGuiColorEditFlags_NoOptions))
5578
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5579
}
5580
5581
ImGuiWindow* picker_active_window = NULL;
5582
if (!(flags & ImGuiColorEditFlags_NoSmallPreview))
5583
{
5584
const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x;
5585
window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y);
5586
5587
const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f);
5588
if (ColorButton("##ColorButton", col_v4, flags))
5589
{
5590
if (!(flags & ImGuiColorEditFlags_NoPicker))
5591
{
5592
// Store current color and open a picker
5593
g.ColorPickerRef = col_v4;
5594
OpenPopup("picker");
5595
SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y));
5596
}
5597
}
5598
if (!(flags & ImGuiColorEditFlags_NoOptions))
5599
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5600
5601
if (BeginPopup("picker"))
5602
{
5603
if (g.CurrentWindow->BeginCount == 1)
5604
{
5605
picker_active_window = g.CurrentWindow;
5606
if (label != label_display_end)
5607
{
5608
TextEx(label, label_display_end);
5609
Spacing();
5610
}
5611
ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar;
5612
ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf;
5613
SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes?
5614
value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
5615
}
5616
EndPopup();
5617
}
5618
}
5619
5620
if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel))
5621
{
5622
// Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left),
5623
// but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this.
5624
SameLine(0.0f, style.ItemInnerSpacing.x);
5625
window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x);
5626
TextEx(label, label_display_end);
5627
}
5628
5629
// Convert back
5630
if (value_changed && picker_active_window == NULL)
5631
{
5632
if (!value_changed_as_float)
5633
for (int n = 0; n < 4; n++)
5634
f[n] = i[n] / 255.0f;
5635
if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB))
5636
{
5637
g.ColorEditSavedHue = f[0];
5638
g.ColorEditSavedSat = f[1];
5639
ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]);
5640
g.ColorEditSavedID = g.ColorEditCurrentID;
5641
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0));
5642
}
5643
if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV))
5644
ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]);
5645
5646
col[0] = f[0];
5647
col[1] = f[1];
5648
col[2] = f[2];
5649
if (alpha)
5650
col[3] = f[3];
5651
}
5652
5653
if (set_current_color_edit_id)
5654
g.ColorEditCurrentID = 0;
5655
PopID();
5656
EndGroup();
5657
5658
// Drag and Drop Target
5659
// NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test.
5660
if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget())
5661
{
5662
bool accepted_drag_drop = false;
5663
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F))
5664
{
5665
memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086
5666
value_changed = accepted_drag_drop = true;
5667
}
5668
if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F))
5669
{
5670
memcpy((float*)col, payload->Data, sizeof(float) * components);
5671
value_changed = accepted_drag_drop = true;
5672
}
5673
5674
// Drag-drop payloads are always RGB
5675
if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV))
5676
ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]);
5677
EndDragDropTarget();
5678
}
5679
5680
// When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4().
5681
if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window)
5682
g.LastItemData.ID = g.ActiveId;
5683
5684
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
5685
MarkItemEdited(g.LastItemData.ID);
5686
5687
return value_changed;
5688
}
5689
5690
bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags)
5691
{
5692
float col4[4] = { col[0], col[1], col[2], 1.0f };
5693
if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha))
5694
return false;
5695
col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2];
5696
return true;
5697
}
5698
5699
// Helper for ColorPicker4()
5700
static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha)
5701
{
5702
ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha);
5703
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));
5704
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8));
5705
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));
5706
ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8));
5707
}
5708
5709
// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
5710
// (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.)
5711
// 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..)
5712
// 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)
5713
bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col)
5714
{
5715
ImGuiContext& g = *GImGui;
5716
ImGuiWindow* window = GetCurrentWindow();
5717
if (window->SkipItems)
5718
return false;
5719
5720
ImDrawList* draw_list = window->DrawList;
5721
ImGuiStyle& style = g.Style;
5722
ImGuiIO& io = g.IO;
5723
5724
const float width = CalcItemWidth();
5725
const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0;
5726
g.NextItemData.ClearFlags();
5727
5728
PushID(label);
5729
const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0);
5730
if (set_current_color_edit_id)
5731
g.ColorEditCurrentID = window->IDStack.back();
5732
BeginGroup();
5733
5734
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5735
flags |= ImGuiColorEditFlags_NoSmallPreview;
5736
5737
// Context menu: display and store options.
5738
if (!(flags & ImGuiColorEditFlags_NoOptions))
5739
ColorPickerOptionsPopup(col, flags);
5740
5741
// Read stored options
5742
if (!(flags & ImGuiColorEditFlags_PickerMask_))
5743
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_;
5744
if (!(flags & ImGuiColorEditFlags_InputMask_))
5745
flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_;
5746
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected
5747
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected
5748
if (!(flags & ImGuiColorEditFlags_NoOptions))
5749
flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar);
5750
5751
// Setup
5752
int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4;
5753
bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha);
5754
ImVec2 picker_pos = window->DC.CursorPos;
5755
float square_sz = GetFrameHeight();
5756
float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars
5757
float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box
5758
float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x;
5759
float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x;
5760
float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f);
5761
5762
float backup_initial_col[4];
5763
memcpy(backup_initial_col, col, components * sizeof(float));
5764
5765
float wheel_thickness = sv_picker_size * 0.08f;
5766
float wheel_r_outer = sv_picker_size * 0.50f;
5767
float wheel_r_inner = wheel_r_outer - wheel_thickness;
5768
ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f);
5769
5770
// Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic.
5771
float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f);
5772
ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point.
5773
ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point.
5774
ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point.
5775
5776
float H = col[0], S = col[1], V = col[2];
5777
float R = col[0], G = col[1], B = col[2];
5778
if (flags & ImGuiColorEditFlags_InputRGB)
5779
{
5780
// Hue is lost when converting from grayscale rgb (saturation=0). Restore it.
5781
ColorConvertRGBtoHSV(R, G, B, H, S, V);
5782
ColorEditRestoreHS(col, &H, &S, &V);
5783
}
5784
else if (flags & ImGuiColorEditFlags_InputHSV)
5785
{
5786
ColorConvertHSVtoRGB(H, S, V, R, G, B);
5787
}
5788
5789
bool value_changed = false, value_changed_h = false, value_changed_sv = false;
5790
5791
PushItemFlag(ImGuiItemFlags_NoNav, true);
5792
if (flags & ImGuiColorEditFlags_PickerHueWheel)
5793
{
5794
// Hue wheel + SV triangle logic
5795
InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size));
5796
if (IsItemActive() && !is_readonly)
5797
{
5798
ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center;
5799
ImVec2 current_off = g.IO.MousePos - wheel_center;
5800
float initial_dist2 = ImLengthSqr(initial_off);
5801
if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1))
5802
{
5803
// Interactive with Hue wheel
5804
H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f;
5805
if (H < 0.0f)
5806
H += 1.0f;
5807
value_changed = value_changed_h = true;
5808
}
5809
float cos_hue_angle = ImCos(-H * 2.0f * IM_PI);
5810
float sin_hue_angle = ImSin(-H * 2.0f * IM_PI);
5811
if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle)))
5812
{
5813
// Interacting with SV triangle
5814
ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle);
5815
if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated))
5816
current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated);
5817
float uu, vv, ww;
5818
ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww);
5819
V = ImClamp(1.0f - vv, 0.0001f, 1.0f);
5820
S = ImClamp(uu / V, 0.0001f, 1.0f);
5821
value_changed = value_changed_sv = true;
5822
}
5823
}
5824
if (!(flags & ImGuiColorEditFlags_NoOptions))
5825
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5826
}
5827
else if (flags & ImGuiColorEditFlags_PickerHueBar)
5828
{
5829
// SV rectangle logic
5830
InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size));
5831
if (IsItemActive() && !is_readonly)
5832
{
5833
S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1));
5834
V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5835
ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square.
5836
value_changed = value_changed_sv = true;
5837
}
5838
if (!(flags & ImGuiColorEditFlags_NoOptions))
5839
OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
5840
5841
// Hue bar logic
5842
SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y));
5843
InvisibleButton("hue", ImVec2(bars_width, sv_picker_size));
5844
if (IsItemActive() && !is_readonly)
5845
{
5846
H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5847
value_changed = value_changed_h = true;
5848
}
5849
}
5850
5851
// Alpha bar logic
5852
if (alpha_bar)
5853
{
5854
SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y));
5855
InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size));
5856
if (IsItemActive())
5857
{
5858
col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1));
5859
value_changed = true;
5860
}
5861
}
5862
PopItemFlag(); // ImGuiItemFlags_NoNav
5863
5864
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5865
{
5866
SameLine(0, style.ItemInnerSpacing.x);
5867
BeginGroup();
5868
}
5869
5870
if (!(flags & ImGuiColorEditFlags_NoLabel))
5871
{
5872
const char* label_display_end = FindRenderedTextEnd(label);
5873
if (label != label_display_end)
5874
{
5875
if ((flags & ImGuiColorEditFlags_NoSidePreview))
5876
SameLine(0, style.ItemInnerSpacing.x);
5877
TextEx(label, label_display_end);
5878
}
5879
}
5880
5881
if (!(flags & ImGuiColorEditFlags_NoSidePreview))
5882
{
5883
PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
5884
ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
5885
if ((flags & ImGuiColorEditFlags_NoLabel))
5886
Text("Current");
5887
5888
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip;
5889
ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
5890
if (ref_col != NULL)
5891
{
5892
Text("Original");
5893
ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
5894
if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
5895
{
5896
memcpy(col, ref_col, components * sizeof(float));
5897
value_changed = true;
5898
}
5899
}
5900
PopItemFlag();
5901
EndGroup();
5902
}
5903
5904
// Convert back color to RGB
5905
if (value_changed_h || value_changed_sv)
5906
{
5907
if (flags & ImGuiColorEditFlags_InputRGB)
5908
{
5909
ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]);
5910
g.ColorEditSavedHue = H;
5911
g.ColorEditSavedSat = S;
5912
g.ColorEditSavedID = g.ColorEditCurrentID;
5913
g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0));
5914
}
5915
else if (flags & ImGuiColorEditFlags_InputHSV)
5916
{
5917
col[0] = H;
5918
col[1] = S;
5919
col[2] = V;
5920
}
5921
}
5922
5923
// R,G,B and H,S,V slider color editor
5924
bool value_changed_fix_hue_wrap = false;
5925
if ((flags & ImGuiColorEditFlags_NoInputs) == 0)
5926
{
5927
PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x);
5928
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf;
5929
ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker;
5930
if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5931
if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB))
5932
{
5933
// FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget.
5934
// 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)
5935
value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
5936
value_changed = true;
5937
}
5938
if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5939
value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV);
5940
if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0)
5941
value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex);
5942
PopItemWidth();
5943
}
5944
5945
// Try to cancel hue wrap (after ColorEdit4 call), if any
5946
if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB))
5947
{
5948
float new_H, new_S, new_V;
5949
ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V);
5950
if (new_H <= 0 && H > 0)
5951
{
5952
if (new_V <= 0 && V != new_V)
5953
ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]);
5954
else if (new_S <= 0)
5955
ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]);
5956
}
5957
}
5958
5959
if (value_changed)
5960
{
5961
if (flags & ImGuiColorEditFlags_InputRGB)
5962
{
5963
R = col[0];
5964
G = col[1];
5965
B = col[2];
5966
ColorConvertRGBtoHSV(R, G, B, H, S, V);
5967
ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately.
5968
}
5969
else if (flags & ImGuiColorEditFlags_InputHSV)
5970
{
5971
H = col[0];
5972
S = col[1];
5973
V = col[2];
5974
ColorConvertHSVtoRGB(H, S, V, R, G, B);
5975
}
5976
}
5977
5978
const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha);
5979
const ImU32 col_black = IM_COL32(0,0,0,style_alpha8);
5980
const ImU32 col_white = IM_COL32(255,255,255,style_alpha8);
5981
const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8);
5982
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) };
5983
5984
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);
5985
ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f);
5986
ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!!
5987
5988
ImVec2 sv_cursor_pos;
5989
5990
if (flags & ImGuiColorEditFlags_PickerHueWheel)
5991
{
5992
// Render Hue Wheel
5993
const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out).
5994
const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12);
5995
for (int n = 0; n < 6; n++)
5996
{
5997
const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps;
5998
const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps;
5999
const int vert_start_idx = draw_list->VtxBuffer.Size;
6000
draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc);
6001
draw_list->PathStroke(col_white, 0, wheel_thickness);
6002
const int vert_end_idx = draw_list->VtxBuffer.Size;
6003
6004
// Paint colors over existing vertices
6005
ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner);
6006
ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner);
6007
ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]);
6008
}
6009
6010
// Render Cursor + preview on Hue Wheel
6011
float cos_hue_angle = ImCos(H * 2.0f * IM_PI);
6012
float sin_hue_angle = ImSin(H * 2.0f * IM_PI);
6013
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);
6014
float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f;
6015
int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others.
6016
draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments);
6017
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments);
6018
draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments);
6019
6020
// Render SV triangle (rotated according to hue)
6021
ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle);
6022
ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle);
6023
ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle);
6024
ImVec2 uv_white = GetFontTexUvWhitePixel();
6025
draw_list->PrimReserve(3, 3);
6026
draw_list->PrimVtx(tra, uv_white, hue_color32);
6027
draw_list->PrimVtx(trb, uv_white, col_black);
6028
draw_list->PrimVtx(trc, uv_white, col_white);
6029
draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f);
6030
sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V));
6031
}
6032
else if (flags & ImGuiColorEditFlags_PickerHueBar)
6033
{
6034
// Render SV Square
6035
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white);
6036
draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black);
6037
RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f);
6038
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
6039
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);
6040
6041
// Render Hue Bar
6042
for (int i = 0; i < 6; ++i)
6043
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]);
6044
float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size);
6045
RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f);
6046
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);
6047
}
6048
6049
// Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range)
6050
float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f;
6051
int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others.
6052
draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments);
6053
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments);
6054
draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments);
6055
6056
// Render alpha bar
6057
if (alpha_bar)
6058
{
6059
float alpha = ImSaturate(col[3]);
6060
ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size);
6061
RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f));
6062
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);
6063
float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size);
6064
RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f);
6065
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);
6066
}
6067
6068
EndGroup();
6069
6070
if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0)
6071
value_changed = false;
6072
if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId
6073
MarkItemEdited(g.LastItemData.ID);
6074
6075
if (set_current_color_edit_id)
6076
g.ColorEditCurrentID = 0;
6077
PopID();
6078
6079
return value_changed;
6080
}
6081
6082
// A little color square. Return true when clicked.
6083
// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip.
6084
// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip.
6085
// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set.
6086
bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg)
6087
{
6088
ImGuiWindow* window = GetCurrentWindow();
6089
if (window->SkipItems)
6090
return false;
6091
6092
ImGuiContext& g = *GImGui;
6093
const ImGuiID id = window->GetID(desc_id);
6094
const float default_size = GetFrameHeight();
6095
const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y);
6096
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size);
6097
ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f);
6098
if (!ItemAdd(bb, id))
6099
return false;
6100
6101
bool hovered, held;
6102
bool pressed = ButtonBehavior(bb, id, &hovered, &held);
6103
6104
if (flags & ImGuiColorEditFlags_NoAlpha)
6105
flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf);
6106
6107
ImVec4 col_rgb = col;
6108
if (flags & ImGuiColorEditFlags_InputHSV)
6109
ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z);
6110
6111
ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f);
6112
float grid_step = ImMin(size.x, size.y) / 2.99f;
6113
float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f);
6114
ImRect bb_inner = bb;
6115
float off = 0.0f;
6116
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6117
{
6118
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.
6119
bb_inner.Expand(off);
6120
}
6121
if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f)
6122
{
6123
float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f);
6124
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);
6125
window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft);
6126
}
6127
else
6128
{
6129
// Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha
6130
ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha;
6131
if (col_source.w < 1.0f)
6132
RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding);
6133
else
6134
window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding);
6135
}
6136
RenderNavCursor(bb, id);
6137
if ((flags & ImGuiColorEditFlags_NoBorder) == 0)
6138
{
6139
if (g.Style.FrameBorderSize > 0.0f)
6140
RenderFrameBorder(bb.Min, bb.Max, rounding);
6141
else
6142
window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border
6143
}
6144
6145
// Drag and Drop Source
6146
// NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test.
6147
if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource())
6148
{
6149
if (flags & ImGuiColorEditFlags_NoAlpha)
6150
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once);
6151
else
6152
SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once);
6153
ColorButton(desc_id, col, flags);
6154
SameLine();
6155
TextEx("Color");
6156
EndDragDropSource();
6157
}
6158
6159
// Tooltip
6160
if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip))
6161
ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf));
6162
6163
return pressed;
6164
}
6165
6166
// Initialize/override default color options
6167
void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags)
6168
{
6169
ImGuiContext& g = *GImGui;
6170
if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0)
6171
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_;
6172
if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0)
6173
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_;
6174
if ((flags & ImGuiColorEditFlags_PickerMask_) == 0)
6175
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_;
6176
if ((flags & ImGuiColorEditFlags_InputMask_) == 0)
6177
flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_;
6178
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected
6179
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected
6180
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected
6181
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected
6182
g.ColorEditOptions = flags;
6183
}
6184
6185
// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set.
6186
void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags)
6187
{
6188
ImGuiContext& g = *GImGui;
6189
6190
if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None))
6191
return;
6192
const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text;
6193
if (text_end > text)
6194
{
6195
TextEx(text, text_end);
6196
Separator();
6197
}
6198
6199
ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2);
6200
ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6201
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]);
6202
ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz);
6203
SameLine();
6204
if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_))
6205
{
6206
if (flags & ImGuiColorEditFlags_NoAlpha)
6207
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]);
6208
else
6209
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]);
6210
}
6211
else if (flags & ImGuiColorEditFlags_InputHSV)
6212
{
6213
if (flags & ImGuiColorEditFlags_NoAlpha)
6214
Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]);
6215
else
6216
Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]);
6217
}
6218
EndTooltip();
6219
}
6220
6221
void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags)
6222
{
6223
bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_);
6224
bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_);
6225
if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context"))
6226
return;
6227
6228
ImGuiContext& g = *GImGui;
6229
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6230
ImGuiColorEditFlags opts = g.ColorEditOptions;
6231
if (allow_opt_inputs)
6232
{
6233
if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB;
6234
if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV;
6235
if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex;
6236
}
6237
if (allow_opt_datatype)
6238
{
6239
if (allow_opt_inputs) Separator();
6240
if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8;
6241
if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float;
6242
}
6243
6244
if (allow_opt_inputs || allow_opt_datatype)
6245
Separator();
6246
if (Button("Copy as..", ImVec2(-1, 0)))
6247
OpenPopup("Copy");
6248
if (BeginPopup("Copy"))
6249
{
6250
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]);
6251
char buf[64];
6252
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
6253
if (Selectable(buf))
6254
SetClipboardText(buf);
6255
ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca);
6256
if (Selectable(buf))
6257
SetClipboardText(buf);
6258
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb);
6259
if (Selectable(buf))
6260
SetClipboardText(buf);
6261
if (!(flags & ImGuiColorEditFlags_NoAlpha))
6262
{
6263
ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca);
6264
if (Selectable(buf))
6265
SetClipboardText(buf);
6266
}
6267
EndPopup();
6268
}
6269
6270
g.ColorEditOptions = opts;
6271
PopItemFlag();
6272
EndPopup();
6273
}
6274
6275
void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags)
6276
{
6277
bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_);
6278
bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar);
6279
if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context"))
6280
return;
6281
6282
ImGuiContext& g = *GImGui;
6283
PushItemFlag(ImGuiItemFlags_NoMarkEdited, true);
6284
if (allow_opt_picker)
6285
{
6286
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
6287
PushItemWidth(picker_size.x);
6288
for (int picker_type = 0; picker_type < 2; picker_type++)
6289
{
6290
// Draw small/thumbnail version of each picker type (over an invisible button for selection)
6291
if (picker_type > 0) Separator();
6292
PushID(picker_type);
6293
ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha);
6294
if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar;
6295
if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel;
6296
ImVec2 backup_pos = GetCursorScreenPos();
6297
if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup
6298
g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
6299
SetCursorScreenPos(backup_pos);
6300
ImVec4 previewing_ref_col;
6301
memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4));
6302
ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags);
6303
PopID();
6304
}
6305
PopItemWidth();
6306
}
6307
if (allow_opt_alpha_bar)
6308
{
6309
if (allow_opt_picker) Separator();
6310
CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar);
6311
}
6312
PopItemFlag();
6313
EndPopup();
6314
}
6315
6316
//-------------------------------------------------------------------------
6317
// [SECTION] Widgets: TreeNode, CollapsingHeader, etc.
6318
//-------------------------------------------------------------------------
6319
// - TreeNode()
6320
// - TreeNodeV()
6321
// - TreeNodeEx()
6322
// - TreeNodeExV()
6323
// - TreeNodeBehavior() [Internal]
6324
// - TreePush()
6325
// - TreePop()
6326
// - GetTreeNodeToLabelSpacing()
6327
// - SetNextItemOpen()
6328
// - CollapsingHeader()
6329
//-------------------------------------------------------------------------
6330
6331
bool ImGui::TreeNode(const char* str_id, const char* fmt, ...)
6332
{
6333
va_list args;
6334
va_start(args, fmt);
6335
bool is_open = TreeNodeExV(str_id, 0, fmt, args);
6336
va_end(args);
6337
return is_open;
6338
}
6339
6340
bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...)
6341
{
6342
va_list args;
6343
va_start(args, fmt);
6344
bool is_open = TreeNodeExV(ptr_id, 0, fmt, args);
6345
va_end(args);
6346
return is_open;
6347
}
6348
6349
bool ImGui::TreeNode(const char* label)
6350
{
6351
ImGuiWindow* window = GetCurrentWindow();
6352
if (window->SkipItems)
6353
return false;
6354
ImGuiID id = window->GetID(label);
6355
return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL);
6356
}
6357
6358
bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args)
6359
{
6360
return TreeNodeExV(str_id, 0, fmt, args);
6361
}
6362
6363
bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args)
6364
{
6365
return TreeNodeExV(ptr_id, 0, fmt, args);
6366
}
6367
6368
bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags)
6369
{
6370
ImGuiWindow* window = GetCurrentWindow();
6371
if (window->SkipItems)
6372
return false;
6373
ImGuiID id = window->GetID(label);
6374
return TreeNodeBehavior(id, flags, label, NULL);
6375
}
6376
6377
bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6378
{
6379
va_list args;
6380
va_start(args, fmt);
6381
bool is_open = TreeNodeExV(str_id, flags, fmt, args);
6382
va_end(args);
6383
return is_open;
6384
}
6385
6386
bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...)
6387
{
6388
va_list args;
6389
va_start(args, fmt);
6390
bool is_open = TreeNodeExV(ptr_id, flags, fmt, args);
6391
va_end(args);
6392
return is_open;
6393
}
6394
6395
bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6396
{
6397
ImGuiWindow* window = GetCurrentWindow();
6398
if (window->SkipItems)
6399
return false;
6400
6401
ImGuiID id = window->GetID(str_id);
6402
const char* label, *label_end;
6403
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6404
return TreeNodeBehavior(id, flags, label, label_end);
6405
}
6406
6407
bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args)
6408
{
6409
ImGuiWindow* window = GetCurrentWindow();
6410
if (window->SkipItems)
6411
return false;
6412
6413
ImGuiID id = window->GetID(ptr_id);
6414
const char* label, *label_end;
6415
ImFormatStringToTempBufferV(&label, &label_end, fmt, args);
6416
return TreeNodeBehavior(id, flags, label, label_end);
6417
}
6418
6419
bool ImGui::TreeNodeGetOpen(ImGuiID storage_id)
6420
{
6421
ImGuiContext& g = *GImGui;
6422
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6423
return storage->GetInt(storage_id, 0) != 0;
6424
}
6425
6426
void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open)
6427
{
6428
ImGuiContext& g = *GImGui;
6429
ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage;
6430
storage->SetInt(storage_id, open ? 1 : 0);
6431
}
6432
6433
bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags)
6434
{
6435
if (flags & ImGuiTreeNodeFlags_Leaf)
6436
return true;
6437
6438
// We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function
6439
ImGuiContext& g = *GImGui;
6440
ImGuiWindow* window = g.CurrentWindow;
6441
ImGuiStorage* storage = window->DC.StateStorage;
6442
6443
bool is_open;
6444
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen)
6445
{
6446
if (g.NextItemData.OpenCond & ImGuiCond_Always)
6447
{
6448
is_open = g.NextItemData.OpenVal;
6449
TreeNodeSetOpen(storage_id, is_open);
6450
}
6451
else
6452
{
6453
// We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently.
6454
const int stored_value = storage->GetInt(storage_id, -1);
6455
if (stored_value == -1)
6456
{
6457
is_open = g.NextItemData.OpenVal;
6458
TreeNodeSetOpen(storage_id, is_open);
6459
}
6460
else
6461
{
6462
is_open = stored_value != 0;
6463
}
6464
}
6465
}
6466
else
6467
{
6468
is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0;
6469
}
6470
6471
// When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior).
6472
// NB- If we are above max depth we still allow manually opened nodes to be logged.
6473
if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand)
6474
is_open = true;
6475
6476
return is_open;
6477
}
6478
6479
// Store ImGuiTreeNodeStackData for just submitted node.
6480
// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase.
6481
static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags)
6482
{
6483
ImGuiContext& g = *GImGui;
6484
ImGuiWindow* window = g.CurrentWindow;
6485
6486
g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1);
6487
ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back();
6488
tree_node_data->ID = g.LastItemData.ID;
6489
tree_node_data->TreeFlags = flags;
6490
tree_node_data->ItemFlags = g.LastItemData.ItemFlags;
6491
tree_node_data->NavRect = g.LastItemData.NavRect;
6492
window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth);
6493
}
6494
6495
// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop.
6496
bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end)
6497
{
6498
ImGuiWindow* window = GetCurrentWindow();
6499
if (window->SkipItems)
6500
return false;
6501
6502
ImGuiContext& g = *GImGui;
6503
const ImGuiStyle& style = g.Style;
6504
const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0;
6505
const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y));
6506
6507
if (!label_end)
6508
label_end = FindRenderedTextEnd(label);
6509
const ImVec2 label_size = CalcTextSize(label, label_end, false);
6510
6511
const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing
6512
const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it
6513
const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow
6514
6515
// We vertically grow up to current line height up the typical widget height.
6516
const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2);
6517
const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL);
6518
ImRect frame_bb;
6519
frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x;
6520
frame_bb.Min.y = window->DC.CursorPos.y;
6521
frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanTextWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x;
6522
frame_bb.Max.y = window->DC.CursorPos.y + frame_height;
6523
if (display_frame)
6524
{
6525
const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits
6526
frame_bb.Min.x -= outer_extend;
6527
frame_bb.Max.x += outer_extend;
6528
}
6529
6530
ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y);
6531
ItemSize(ImVec2(text_width, frame_height), padding.y);
6532
6533
// For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing
6534
ImRect interact_bb = frame_bb;
6535
if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanTextWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0)
6536
interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f);
6537
6538
// Compute open and multi-select states before ItemAdd() as it clear NextItem data.
6539
ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id;
6540
bool is_open = TreeNodeUpdateNextOpen(storage_id, flags);
6541
6542
bool is_visible;
6543
if (span_all_columns)
6544
{
6545
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6546
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6547
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6548
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6549
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6550
is_visible = ItemAdd(interact_bb, id);
6551
window->ClipRect.Min.x = backup_clip_rect_min_x;
6552
window->ClipRect.Max.x = backup_clip_rect_max_x;
6553
}
6554
else
6555
{
6556
is_visible = ItemAdd(interact_bb, id);
6557
}
6558
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect;
6559
g.LastItemData.DisplayRect = frame_bb;
6560
6561
// If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled:
6562
// Store data for the current depth to allow returning to this node from any child item.
6563
// For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop().
6564
// It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle.
6565
bool store_tree_node_stack_data = false;
6566
if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6567
{
6568
if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive)
6569
if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6570
store_tree_node_stack_data = true;
6571
}
6572
6573
const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0;
6574
if (!is_visible)
6575
{
6576
if (store_tree_node_stack_data && is_open)
6577
TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6578
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6579
TreePushOverrideID(id);
6580
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6581
return is_open;
6582
}
6583
6584
if (span_all_columns)
6585
{
6586
TablePushBackgroundChannel();
6587
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6588
g.LastItemData.ClipRect = window->ClipRect;
6589
}
6590
6591
ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None;
6592
if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap))
6593
button_flags |= ImGuiButtonFlags_AllowOverlap;
6594
if (!is_leaf)
6595
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
6596
6597
// We allow clicking on the arrow section with keyboard modifiers held, in order to easily
6598
// allow browsing a tree while preserving selection with code implementing multi-selection patterns.
6599
// When clicking on the rest of the tree node we always disallow keyboard modifiers.
6600
const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x;
6601
const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x;
6602
const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2);
6603
6604
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6605
if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default
6606
flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow;
6607
6608
// Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags.
6609
// Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support.
6610
// - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0)
6611
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0)
6612
// - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1)
6613
// - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1)
6614
// - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0)
6615
// It is rather standard that arrow click react on Down rather than Up.
6616
// 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.
6617
if (is_mouse_x_over_arrow)
6618
button_flags |= ImGuiButtonFlags_PressedOnClick;
6619
else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick)
6620
button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick;
6621
else
6622
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
6623
6624
bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0;
6625
const bool was_selected = selected;
6626
6627
// Multi-selection support (header)
6628
if (is_multi_select)
6629
{
6630
// Handle multi-select + alter button flags for it
6631
MultiSelectItemHeader(id, &selected, &button_flags);
6632
if (is_mouse_x_over_arrow)
6633
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
6634
}
6635
else
6636
{
6637
if (window != g.HoveredWindow || !is_mouse_x_over_arrow)
6638
button_flags |= ImGuiButtonFlags_NoKeyModsAllowed;
6639
}
6640
6641
bool hovered, held;
6642
bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags);
6643
bool toggled = false;
6644
if (!is_leaf)
6645
{
6646
if (pressed && g.DragDropHoldJustPressedId != id)
6647
{
6648
if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select))
6649
toggled = true; // Single click
6650
if (flags & ImGuiTreeNodeFlags_OpenOnArrow)
6651
toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job
6652
if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2)
6653
toggled = true; // Double click
6654
}
6655
else if (pressed && g.DragDropHoldJustPressedId == id)
6656
{
6657
IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold);
6658
if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again.
6659
toggled = true;
6660
else
6661
pressed = false; // Cancel press so it doesn't trigger selection.
6662
}
6663
6664
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open)
6665
{
6666
toggled = true;
6667
NavClearPreferredPosForAxis(ImGuiAxis_X);
6668
NavMoveRequestCancel();
6669
}
6670
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?
6671
{
6672
toggled = true;
6673
NavClearPreferredPosForAxis(ImGuiAxis_X);
6674
NavMoveRequestCancel();
6675
}
6676
6677
if (toggled)
6678
{
6679
is_open = !is_open;
6680
window->DC.StateStorage->SetInt(storage_id, is_open);
6681
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen;
6682
}
6683
}
6684
6685
// Multi-selection support (footer)
6686
if (is_multi_select)
6687
{
6688
bool pressed_copy = pressed && !toggled;
6689
MultiSelectItemFooter(id, &selected, &pressed_copy);
6690
if (pressed)
6691
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb);
6692
}
6693
6694
if (selected != was_selected)
6695
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
6696
6697
// Render
6698
{
6699
const ImU32 text_col = GetColorU32(ImGuiCol_Text);
6700
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact;
6701
if (is_multi_select)
6702
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
6703
if (display_frame)
6704
{
6705
// Framed type
6706
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6707
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding);
6708
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6709
if (flags & ImGuiTreeNodeFlags_Bullet)
6710
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col);
6711
else if (!is_leaf)
6712
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);
6713
else // Leaf without bullet, left-adjusted text
6714
text_pos.x -= text_offset_x - padding.x;
6715
if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton)
6716
frame_bb.Max.x -= g.FontSize + style.FramePadding.x;
6717
if (g.LogEnabled)
6718
LogSetNextTextDecoration("###", "###");
6719
}
6720
else
6721
{
6722
// Unframed typed for tree nodes
6723
if (hovered || selected)
6724
{
6725
const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
6726
RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false);
6727
}
6728
RenderNavCursor(frame_bb, id, nav_render_cursor_flags);
6729
if (flags & ImGuiTreeNodeFlags_Bullet)
6730
RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col);
6731
else if (!is_leaf)
6732
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);
6733
if (g.LogEnabled)
6734
LogSetNextTextDecoration(">", NULL);
6735
}
6736
6737
if (span_all_columns)
6738
TablePopBackgroundChannel();
6739
6740
// Label
6741
if (display_frame)
6742
RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size);
6743
else
6744
RenderText(text_pos, label, label_end, false);
6745
}
6746
6747
if (store_tree_node_stack_data && is_open)
6748
TreeNodeStoreStackData(flags); // Call before TreePushOverrideID()
6749
if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen))
6750
TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice
6751
6752
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0));
6753
return is_open;
6754
}
6755
6756
void ImGui::TreePush(const char* str_id)
6757
{
6758
ImGuiWindow* window = GetCurrentWindow();
6759
Indent();
6760
window->DC.TreeDepth++;
6761
PushID(str_id);
6762
}
6763
6764
void ImGui::TreePush(const void* ptr_id)
6765
{
6766
ImGuiWindow* window = GetCurrentWindow();
6767
Indent();
6768
window->DC.TreeDepth++;
6769
PushID(ptr_id);
6770
}
6771
6772
void ImGui::TreePushOverrideID(ImGuiID id)
6773
{
6774
ImGuiContext& g = *GImGui;
6775
ImGuiWindow* window = g.CurrentWindow;
6776
Indent();
6777
window->DC.TreeDepth++;
6778
PushOverrideID(id);
6779
}
6780
6781
void ImGui::TreePop()
6782
{
6783
ImGuiContext& g = *GImGui;
6784
ImGuiWindow* window = g.CurrentWindow;
6785
Unindent();
6786
6787
window->DC.TreeDepth--;
6788
ImU32 tree_depth_mask = (1 << window->DC.TreeDepth);
6789
6790
if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request
6791
{
6792
ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back();
6793
IM_ASSERT(data->ID == window->IDStack.back());
6794
if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere)
6795
{
6796
// Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled)
6797
if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet())
6798
NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data);
6799
}
6800
g.TreeNodeStack.pop_back();
6801
window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask;
6802
}
6803
6804
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.
6805
PopID();
6806
}
6807
6808
// Horizontal distance preceding label when using TreeNode() or Bullet()
6809
float ImGui::GetTreeNodeToLabelSpacing()
6810
{
6811
ImGuiContext& g = *GImGui;
6812
return g.FontSize + (g.Style.FramePadding.x * 2.0f);
6813
}
6814
6815
// Set next TreeNode/CollapsingHeader open state.
6816
void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond)
6817
{
6818
ImGuiContext& g = *GImGui;
6819
if (g.CurrentWindow->SkipItems)
6820
return;
6821
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen;
6822
g.NextItemData.OpenVal = is_open;
6823
g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always);
6824
}
6825
6826
// Set next TreeNode/CollapsingHeader storage id.
6827
void ImGui::SetNextItemStorageID(ImGuiID storage_id)
6828
{
6829
ImGuiContext& g = *GImGui;
6830
if (g.CurrentWindow->SkipItems)
6831
return;
6832
g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID;
6833
g.NextItemData.StorageId = storage_id;
6834
}
6835
6836
// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag).
6837
// 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().
6838
bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags)
6839
{
6840
ImGuiWindow* window = GetCurrentWindow();
6841
if (window->SkipItems)
6842
return false;
6843
ImGuiID id = window->GetID(label);
6844
return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label);
6845
}
6846
6847
// p_visible == NULL : regular collapsing header
6848
// 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
6849
// p_visible != NULL && *p_visible == false : do not show the header at all
6850
// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen.
6851
bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags)
6852
{
6853
ImGuiWindow* window = GetCurrentWindow();
6854
if (window->SkipItems)
6855
return false;
6856
6857
if (p_visible && !*p_visible)
6858
return false;
6859
6860
ImGuiID id = window->GetID(label);
6861
flags |= ImGuiTreeNodeFlags_CollapsingHeader;
6862
if (p_visible)
6863
flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton;
6864
bool is_open = TreeNodeBehavior(id, flags, label);
6865
if (p_visible != NULL)
6866
{
6867
// Create a small overlapping close button
6868
// FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc.
6869
// FIXME: CloseButton can overlap into text, need find a way to clip the text somehow.
6870
ImGuiContext& g = *GImGui;
6871
ImGuiLastItemData last_item_backup = g.LastItemData;
6872
float button_size = g.FontSize;
6873
float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size);
6874
float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y;
6875
ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id);
6876
if (CloseButton(close_button_id, ImVec2(button_x, button_y)))
6877
*p_visible = false;
6878
g.LastItemData = last_item_backup;
6879
}
6880
6881
return is_open;
6882
}
6883
6884
//-------------------------------------------------------------------------
6885
// [SECTION] Widgets: Selectable
6886
//-------------------------------------------------------------------------
6887
// - Selectable()
6888
//-------------------------------------------------------------------------
6889
6890
// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image.
6891
// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id.
6892
// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags.
6893
// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported.
6894
bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
6895
{
6896
ImGuiWindow* window = GetCurrentWindow();
6897
if (window->SkipItems)
6898
return false;
6899
6900
ImGuiContext& g = *GImGui;
6901
const ImGuiStyle& style = g.Style;
6902
6903
// Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle.
6904
ImGuiID id = window->GetID(label);
6905
ImVec2 label_size = CalcTextSize(label, NULL, true);
6906
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
6907
ImVec2 pos = window->DC.CursorPos;
6908
pos.y += window->DC.CurrLineTextBaseOffset;
6909
ItemSize(size, 0.0f);
6910
6911
// Fill horizontal space
6912
// We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
6913
const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
6914
const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
6915
const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
6916
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
6917
size.x = ImMax(label_size.x, max_x - min_x);
6918
6919
// Text stays at the submission position, but bounding box may be extended on both sides
6920
const ImVec2 text_min = pos;
6921
const ImVec2 text_max(min_x + size.x, pos.y + size.y);
6922
6923
// Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
6924
// FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos.
6925
ImRect bb(min_x, pos.y, text_max.x, text_max.y);
6926
if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0)
6927
{
6928
const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x;
6929
const float spacing_y = style.ItemSpacing.y;
6930
const float spacing_L = IM_TRUNC(spacing_x * 0.50f);
6931
const float spacing_U = IM_TRUNC(spacing_y * 0.50f);
6932
bb.Min.x -= spacing_L;
6933
bb.Min.y -= spacing_U;
6934
bb.Max.x += (spacing_x - spacing_L);
6935
bb.Max.y += (spacing_y - spacing_U);
6936
}
6937
//if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); }
6938
6939
const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0;
6940
const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None;
6941
bool is_visible;
6942
if (span_all_columns)
6943
{
6944
// Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable..
6945
const float backup_clip_rect_min_x = window->ClipRect.Min.x;
6946
const float backup_clip_rect_max_x = window->ClipRect.Max.x;
6947
window->ClipRect.Min.x = window->ParentWorkRect.Min.x;
6948
window->ClipRect.Max.x = window->ParentWorkRect.Max.x;
6949
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
6950
window->ClipRect.Min.x = backup_clip_rect_min_x;
6951
window->ClipRect.Max.x = backup_clip_rect_max_x;
6952
}
6953
else
6954
{
6955
is_visible = ItemAdd(bb, id, NULL, extra_item_flags);
6956
}
6957
6958
const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0;
6959
if (!is_visible)
6960
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)
6961
return false;
6962
6963
const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0;
6964
if (disabled_item && !disabled_global) // Only testing this as an optimization
6965
BeginDisabled();
6966
6967
// FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only,
6968
// which would be advantageous since most selectable are not selected.
6969
if (span_all_columns)
6970
{
6971
if (g.CurrentTable)
6972
TablePushBackgroundChannel();
6973
else if (window->DC.CurrentColumns)
6974
PushColumnsBackground();
6975
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect;
6976
g.LastItemData.ClipRect = window->ClipRect;
6977
}
6978
6979
// We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries
6980
ImGuiButtonFlags button_flags = 0;
6981
if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; }
6982
if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; }
6983
if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; }
6984
if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; }
6985
if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; }
6986
if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; }
6987
6988
// Multi-selection support (header)
6989
const bool was_selected = selected;
6990
if (is_multi_select)
6991
{
6992
// Handle multi-select + alter button flags for it
6993
MultiSelectItemHeader(id, &selected, &button_flags);
6994
}
6995
6996
bool hovered, held;
6997
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
6998
6999
// Multi-selection support (footer)
7000
if (is_multi_select)
7001
{
7002
MultiSelectItemFooter(id, &selected, &pressed);
7003
}
7004
else
7005
{
7006
// Auto-select when moved into
7007
// - This will be more fully fleshed in the range-select branch
7008
// - This is not exposed as it won't nicely work with some user side handling of shift/control
7009
// - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons
7010
// - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope())
7011
// - (2) usage will fail with clipped items
7012
// The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API.
7013
if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId)
7014
if (g.NavJustMovedToId == id)
7015
selected = pressed = true;
7016
}
7017
7018
// Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad
7019
if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover)))
7020
{
7021
if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent)
7022
{
7023
SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect)
7024
if (g.IO.ConfigNavCursorVisibleAuto)
7025
g.NavCursorVisible = false;
7026
}
7027
}
7028
if (pressed)
7029
MarkItemEdited(id);
7030
7031
if (selected != was_selected)
7032
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection;
7033
7034
// Render
7035
if (is_visible)
7036
{
7037
const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight);
7038
if (highlighted || selected)
7039
{
7040
// Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106)
7041
ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header);
7042
RenderFrame(bb.Min, bb.Max, col, false, 0.0f);
7043
}
7044
if (g.NavId == id)
7045
{
7046
ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding;
7047
if (is_multi_select)
7048
nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle
7049
RenderNavCursor(bb, id, nav_render_cursor_flags);
7050
}
7051
}
7052
7053
if (span_all_columns)
7054
{
7055
if (g.CurrentTable)
7056
TablePopBackgroundChannel();
7057
else if (window->DC.CurrentColumns)
7058
PopColumnsBackground();
7059
}
7060
7061
if (is_visible)
7062
RenderTextClipped(text_min, text_max, label, NULL, &label_size, style.SelectableTextAlign, &bb);
7063
7064
// Automatically close popups
7065
if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups))
7066
CloseCurrentPopup();
7067
7068
if (disabled_item && !disabled_global)
7069
EndDisabled();
7070
7071
// Selectable() always returns a pressed state!
7072
// Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve
7073
// selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect().
7074
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
7075
return pressed; //-V1020
7076
}
7077
7078
bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg)
7079
{
7080
if (Selectable(label, *p_selected, flags, size_arg))
7081
{
7082
*p_selected = !*p_selected;
7083
return true;
7084
}
7085
return false;
7086
}
7087
7088
7089
//-------------------------------------------------------------------------
7090
// [SECTION] Widgets: Typing-Select support
7091
//-------------------------------------------------------------------------
7092
7093
// [Experimental] Currently not exposed in public API.
7094
// Consume character inputs and return search request, if any.
7095
// This would typically only be called on the focused window or location you want to grab inputs for, e.g.
7096
// if (ImGui::IsWindowFocused(...))
7097
// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest())
7098
// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1);
7099
// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer).
7100
ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags)
7101
{
7102
ImGuiContext& g = *GImGui;
7103
ImGuiTypingSelectState* data = &g.TypingSelectState;
7104
ImGuiTypingSelectRequest* out_request = &data->Request;
7105
7106
// Clear buffer
7107
const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config.
7108
const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times
7109
if (data->SearchBuffer[0] != 0)
7110
{
7111
bool clear_buffer = false;
7112
clear_buffer |= (g.NavFocusScopeId != data->FocusScope);
7113
clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time);
7114
clear_buffer |= g.NavAnyRequest;
7115
clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere
7116
clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter);
7117
clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0;
7118
//if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); }
7119
if (clear_buffer)
7120
data->Clear();
7121
}
7122
7123
// Append to buffer
7124
const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1;
7125
int buffer_len = (int)strlen(data->SearchBuffer);
7126
bool select_request = false;
7127
for (ImWchar w : g.IO.InputQueueCharacters)
7128
{
7129
const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1);
7130
if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks
7131
continue;
7132
char w_buf[5];
7133
ImTextCharToUtf8(w_buf, (unsigned int)w);
7134
if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0)
7135
{
7136
select_request = true; // Same character: don't need to append to buffer.
7137
continue;
7138
}
7139
if (data->SingleCharModeLock)
7140
{
7141
data->Clear(); // Different character: clear
7142
buffer_len = 0;
7143
}
7144
memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append
7145
buffer_len += w_len;
7146
select_request = true;
7147
}
7148
g.IO.InputQueueCharacters.resize(0);
7149
7150
// Handle backspace
7151
if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat))
7152
{
7153
char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len);
7154
*p = 0;
7155
buffer_len = (int)(p - data->SearchBuffer);
7156
}
7157
7158
// Return request if any
7159
if (buffer_len == 0)
7160
return NULL;
7161
if (select_request)
7162
{
7163
data->FocusScope = g.NavFocusScopeId;
7164
data->LastRequestFrame = g.FrameCount;
7165
data->LastRequestTime = (float)g.Time;
7166
}
7167
out_request->Flags = flags;
7168
out_request->SearchBufferLen = buffer_len;
7169
out_request->SearchBuffer = data->SearchBuffer;
7170
out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount);
7171
out_request->SingleCharMode = false;
7172
out_request->SingleCharSize = 0;
7173
7174
// Calculate if buffer contains the same character repeated.
7175
// - This can be used to implement a special search mode on first character.
7176
// - Performed on UTF-8 codepoint for correctness.
7177
// - SingleCharMode is always set for first input character, because it usually leads to a "next".
7178
if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode)
7179
{
7180
const char* buf_begin = out_request->SearchBuffer;
7181
const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen;
7182
const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end);
7183
const char* p = buf_begin + c0_len;
7184
for (; p < buf_end; p += c0_len)
7185
if (memcmp(buf_begin, p, (size_t)c0_len) != 0)
7186
break;
7187
const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0;
7188
out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock);
7189
out_request->SingleCharSize = (ImS8)c0_len;
7190
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.
7191
}
7192
7193
return out_request;
7194
}
7195
7196
static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2)
7197
{
7198
int match_len = 0;
7199
while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++))
7200
match_len++;
7201
return match_len;
7202
}
7203
7204
// Default handler for finding a result for typing-select. You may implement your own.
7205
// You might want to display a tooltip to visualize the current request SearchBuffer
7206
// When SingleCharMode is set:
7207
// - it is better to NOT display a tooltip of other on-screen display indicator.
7208
// - the index of the currently focused item is required.
7209
// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData.
7210
int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7211
{
7212
if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot.
7213
return -1;
7214
int idx = -1;
7215
if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode))
7216
idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx);
7217
else
7218
idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data);
7219
if (idx != -1)
7220
SetNavCursorVisibleAfterMove();
7221
return idx;
7222
}
7223
7224
// Special handling when a single character is repeated: perform search on a single letter and goes to next.
7225
int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx)
7226
{
7227
// FIXME: Assume selection user data is index. Would be extremely practical.
7228
//if (nav_item_idx == -1)
7229
// nav_item_idx = (int)g.NavLastValidSelectionUserData;
7230
7231
int first_match_idx = -1;
7232
bool return_next_match = false;
7233
for (int idx = 0; idx < items_count; idx++)
7234
{
7235
const char* item_name = get_item_name_func(user_data, idx);
7236
if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize)
7237
continue;
7238
if (return_next_match) // Return next matching item after current item.
7239
return idx;
7240
if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value.
7241
return idx;
7242
if (first_match_idx == -1) // Record first match for wrapping.
7243
first_match_idx = idx;
7244
if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match.
7245
return_next_match = true;
7246
}
7247
return first_match_idx; // First result
7248
}
7249
7250
int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data)
7251
{
7252
int longest_match_idx = -1;
7253
int longest_match_len = 0;
7254
for (int idx = 0; idx < items_count; idx++)
7255
{
7256
const char* item_name = get_item_name_func(user_data, idx);
7257
const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name);
7258
if (match_len <= longest_match_len)
7259
continue;
7260
longest_match_idx = idx;
7261
longest_match_len = match_len;
7262
if (match_len == req->SearchBufferLen)
7263
break;
7264
}
7265
return longest_match_idx;
7266
}
7267
7268
void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data)
7269
{
7270
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
7271
Text("SearchBuffer = \"%s\"", data->SearchBuffer);
7272
Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock);
7273
Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame);
7274
#else
7275
IM_UNUSED(data);
7276
#endif
7277
}
7278
7279
//-------------------------------------------------------------------------
7280
// [SECTION] Widgets: Box-Select support
7281
// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet.
7282
//-------------------------------------------------------------------------
7283
// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step()
7284
//-------------------------------------------------------------------------
7285
// - BoxSelectPreStartDrag() [Internal]
7286
// - BoxSelectActivateDrag() [Internal]
7287
// - BoxSelectDeactivateDrag() [Internal]
7288
// - BoxSelectScrollWithMouseDrag() [Internal]
7289
// - BeginBoxSelect() [Internal]
7290
// - EndBoxSelect() [Internal]
7291
//-------------------------------------------------------------------------
7292
7293
// Call on the initial click.
7294
static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item)
7295
{
7296
ImGuiContext& g = *GImGui;
7297
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7298
bs->ID = id;
7299
bs->IsStarting = true; // Consider starting box-select.
7300
bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid);
7301
bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid;
7302
bs->KeyMods = g.IO.KeyMods;
7303
bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos);
7304
bs->ScrollAccum = ImVec2(0.0f, 0.0f);
7305
}
7306
7307
static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window)
7308
{
7309
ImGuiContext& g = *GImGui;
7310
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID);
7311
bs->IsActive = true;
7312
bs->Window = window;
7313
bs->IsStarting = false;
7314
ImGui::SetActiveID(bs->ID, window);
7315
ImGui::SetActiveIdUsingAllKeyboardKeys();
7316
if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0)
7317
bs->RequestClear = true;
7318
}
7319
7320
static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs)
7321
{
7322
ImGuiContext& g = *GImGui;
7323
bs->IsActive = bs->IsStarting = false;
7324
if (g.ActiveId == bs->ID)
7325
{
7326
IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID);
7327
ImGui::ClearActiveID();
7328
}
7329
bs->ID = 0;
7330
}
7331
7332
static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r)
7333
{
7334
ImGuiContext& g = *GImGui;
7335
IM_ASSERT(bs->Window == window);
7336
for (int n = 0; n < 2; n++) // each axis
7337
{
7338
const float mouse_pos = g.IO.MousePos[n];
7339
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;
7340
const float scroll_curr = window->Scroll[n];
7341
if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n]))
7342
continue;
7343
7344
const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance
7345
const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime;
7346
bs->ScrollAccum[n] += scroll_step;
7347
7348
// Accumulate into a stored value so we can handle high-framerate
7349
const float scroll_step_i = ImFloor(bs->ScrollAccum[n]);
7350
if (scroll_step_i == 0.0f)
7351
continue;
7352
if (n == 0)
7353
ImGui::SetScrollX(window, scroll_curr + scroll_step_i);
7354
else
7355
ImGui::SetScrollY(window, scroll_curr + scroll_step_i);
7356
bs->ScrollAccum[n] -= scroll_step_i;
7357
}
7358
}
7359
7360
bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags)
7361
{
7362
ImGuiContext& g = *GImGui;
7363
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7364
KeepAliveID(box_select_id);
7365
if (bs->ID != box_select_id)
7366
return false;
7367
7368
// IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry.
7369
bs->UnclipMode = false;
7370
bs->RequestClear = false;
7371
if (bs->IsStarting && IsMouseDragPastThreshold(0))
7372
BoxSelectActivateDrag(bs, window);
7373
else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false)
7374
BoxSelectDeactivateDrag(bs);
7375
if (!bs->IsActive)
7376
return false;
7377
7378
// Current frame absolute prev/current rectangles are used to toggle selection.
7379
// They are derived from positions relative to scrolling space.
7380
ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel);
7381
ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already
7382
ImVec2 curr_end_pos_abs = g.IO.MousePos;
7383
if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow
7384
curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max);
7385
bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs);
7386
bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs);
7387
bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs);
7388
bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs);
7389
7390
// Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper)
7391
// Storing an extra rect used by widgets supporting box-select.
7392
if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d)
7393
if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x)
7394
{
7395
bs->UnclipMode = true;
7396
bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis.
7397
bs->UnclipRect.Add(bs->BoxSelectRectCurr);
7398
}
7399
7400
//GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7401
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f);
7402
//GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f);
7403
return true;
7404
}
7405
7406
void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags)
7407
{
7408
ImGuiContext& g = *GImGui;
7409
ImGuiWindow* window = g.CurrentWindow;
7410
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7411
IM_ASSERT(bs->IsActive);
7412
bs->UnclipMode = false;
7413
7414
// Render selection rectangle
7415
bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view
7416
ImRect box_select_r = bs->BoxSelectRectCurr;
7417
box_select_r.ClipWith(scope_rect);
7418
window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling
7419
window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling
7420
7421
// Scroll
7422
const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0;
7423
if (enable_scroll)
7424
{
7425
ImRect scroll_r = scope_rect;
7426
scroll_r.Expand(-g.FontSize);
7427
//GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255));
7428
if (!scroll_r.Contains(g.IO.MousePos))
7429
BoxSelectScrollWithMouseDrag(bs, window, scroll_r);
7430
}
7431
}
7432
7433
//-------------------------------------------------------------------------
7434
// [SECTION] Widgets: Multi-Select support
7435
//-------------------------------------------------------------------------
7436
// - DebugLogMultiSelectRequests() [Internal]
7437
// - CalcScopeRect() [Internal]
7438
// - BeginMultiSelect()
7439
// - EndMultiSelect()
7440
// - SetNextItemSelectionUserData()
7441
// - MultiSelectItemHeader() [Internal]
7442
// - MultiSelectItemFooter() [Internal]
7443
// - DebugNodeMultiSelectState() [Internal]
7444
//-------------------------------------------------------------------------
7445
7446
static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io)
7447
{
7448
ImGuiContext& g = *GImGui;
7449
IM_UNUSED(function);
7450
for (const ImGuiSelectionRequest& req : io->Requests)
7451
{
7452
if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear");
7453
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);
7454
}
7455
}
7456
7457
static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window)
7458
{
7459
ImGuiContext& g = *GImGui;
7460
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7461
{
7462
// Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only
7463
return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin));
7464
}
7465
else
7466
{
7467
// When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970)
7468
ImRect scope_rect = window->InnerClipRect;
7469
if (g.CurrentTable != NULL)
7470
scope_rect = g.CurrentTable->HostClipRect;
7471
7472
// Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect?
7473
scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max);
7474
return scope_rect;
7475
}
7476
}
7477
7478
// Return ImGuiMultiSelectIO structure.
7479
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7480
// Passing 'selection_size' and 'items_count' parameters is currently optional.
7481
// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0,
7482
// allowing a first press to clear selection THEN the second press to leave child window and return to parent.
7483
// - '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).
7484
// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1.
7485
// - 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.
7486
ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count)
7487
{
7488
ImGuiContext& g = *GImGui;
7489
ImGuiWindow* window = g.CurrentWindow;
7490
7491
if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size)
7492
g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData());
7493
ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1];
7494
IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that.
7495
g.CurrentMultiSelect = ms;
7496
if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0)
7497
flags |= ImGuiMultiSelectFlags_ScopeWindow;
7498
if (flags & ImGuiMultiSelectFlags_SingleSelect)
7499
flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d);
7500
if (flags & ImGuiMultiSelectFlags_BoxSelect2d)
7501
flags &= ~ImGuiMultiSelectFlags_BoxSelect1d;
7502
7503
// FIXME: BeginFocusScope()
7504
const ImGuiID id = window->IDStack.back();
7505
ms->Clear();
7506
ms->FocusScopeId = id;
7507
ms->Flags = flags;
7508
ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId);
7509
ms->BackupCursorMaxPos = window->DC.CursorMaxPos;
7510
ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos;
7511
PushFocusScope(ms->FocusScopeId);
7512
if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items.
7513
window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main;
7514
7515
// Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame.
7516
ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods;
7517
if (flags & ImGuiMultiSelectFlags_NoRangeSelect)
7518
ms->KeyMods &= ~ImGuiMod_Shift;
7519
7520
// Bind storage
7521
ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id);
7522
storage->ID = id;
7523
storage->LastFrameActive = g.FrameCount;
7524
storage->LastSelectionSize = selection_size;
7525
storage->Window = window;
7526
ms->Storage = storage;
7527
7528
// Output to user
7529
ms->IO.Requests.resize(0);
7530
ms->IO.RangeSrcItem = storage->RangeSrcItem;
7531
ms->IO.NavIdItem = storage->NavIdItem;
7532
ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false;
7533
ms->IO.ItemsCount = items_count;
7534
7535
// Clear when using Navigation to move within the scope
7536
// (we compare FocusScopeId so it possible to use multiple selections inside a same window)
7537
bool request_clear = false;
7538
bool request_select_all = false;
7539
if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData)
7540
{
7541
if (ms->KeyMods & ImGuiMod_Shift)
7542
ms->IsKeyboardSetRange = true;
7543
if (ms->IsKeyboardSetRange)
7544
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear?
7545
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7546
request_clear = true;
7547
}
7548
else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId)
7549
{
7550
// Also clear on leaving scope (may be optional?)
7551
if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0)
7552
request_clear = true;
7553
}
7554
7555
// Box-select handling: update active state.
7556
ImGuiBoxSelectState* bs = &g.BoxSelectState;
7557
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7558
{
7559
ms->BoxSelectId = GetID("##BoxSelect");
7560
if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags))
7561
request_clear |= bs->RequestClear;
7562
}
7563
7564
if (ms->IsFocused)
7565
{
7566
// Shortcut: Clear selection (Escape)
7567
// - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window.
7568
// - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock.
7569
if (flags & ImGuiMultiSelectFlags_ClearOnEscape)
7570
{
7571
if (selection_size != 0 || bs->IsActive)
7572
if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0))
7573
{
7574
request_clear = true;
7575
if (bs->IsActive)
7576
BoxSelectDeactivateDrag(bs);
7577
}
7578
}
7579
7580
// Shortcut: Select all (CTRL+A)
7581
if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll))
7582
if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A))
7583
request_select_all = true;
7584
}
7585
7586
if (request_clear || request_select_all)
7587
{
7588
MultiSelectAddSetAll(ms, request_select_all);
7589
if (!request_select_all)
7590
storage->LastSelectionSize = 0;
7591
}
7592
ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1;
7593
ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid;
7594
7595
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7596
DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO);
7597
7598
return &ms->IO;
7599
}
7600
7601
// Return updated ImGuiMultiSelectIO structure.
7602
// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect().
7603
ImGuiMultiSelectIO* ImGui::EndMultiSelect()
7604
{
7605
ImGuiContext& g = *GImGui;
7606
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7607
ImGuiMultiSelectState* storage = ms->Storage;
7608
ImGuiWindow* window = g.CurrentWindow;
7609
IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!");
7610
IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow);
7611
IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect);
7612
7613
ImRect scope_rect = CalcScopeRect(ms, window);
7614
if (ms->IsFocused)
7615
{
7616
// We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here.
7617
if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure)
7618
{
7619
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId.
7620
storage->RangeSrcItem = ImGuiSelectionUserData_Invalid;
7621
}
7622
if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid)
7623
{
7624
IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n");
7625
storage->NavIdItem = ImGuiSelectionUserData_Invalid;
7626
storage->NavIdSelected = -1;
7627
}
7628
7629
if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId))
7630
EndBoxSelect(scope_rect, ms->Flags);
7631
}
7632
7633
if (ms->IsEndIO == false)
7634
ms->IO.Requests.resize(0);
7635
7636
// Clear selection when clicking void?
7637
// We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection!
7638
// The InnerRect test is necessary for non-child/decorated windows.
7639
bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos);
7640
if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect))
7641
scope_hovered &= scope_rect.Contains(g.IO.MousePos);
7642
if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0)
7643
{
7644
if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7645
{
7646
if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1)
7647
{
7648
BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid);
7649
FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal);
7650
SetHoveredID(ms->BoxSelectId);
7651
if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)
7652
SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select.
7653
}
7654
}
7655
7656
if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid)
7657
if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None)
7658
MultiSelectAddSetAll(ms, false);
7659
}
7660
7661
// Courtesy nav wrapping helper flag
7662
if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX)
7663
{
7664
IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope
7665
ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX);
7666
}
7667
7668
// Unwind
7669
window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos);
7670
PopFocusScope();
7671
7672
if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection)
7673
DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO);
7674
7675
ms->FocusScopeId = 0;
7676
ms->Flags = ImGuiMultiSelectFlags_None;
7677
g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL;
7678
7679
return &ms->IO;
7680
}
7681
7682
void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data)
7683
{
7684
// Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code!
7685
// This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api.
7686
ImGuiContext& g = *GImGui;
7687
g.NextItemData.SelectionUserData = selection_user_data;
7688
g.NextItemData.FocusScopeId = g.CurrentFocusScopeId;
7689
7690
if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect)
7691
{
7692
// Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping)
7693
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect;
7694
if (ms->IO.RangeSrcItem == selection_user_data)
7695
ms->RangeSrcPassedBy = true;
7696
}
7697
else
7698
{
7699
g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData;
7700
}
7701
}
7702
7703
// In charge of:
7704
// - Applying SetAll for submitted items.
7705
// - Applying SetRange for submitted items and record end points.
7706
// - Altering button behavior flags to facilitate use with drag and drop.
7707
void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags)
7708
{
7709
ImGuiContext& g = *GImGui;
7710
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7711
7712
bool selected = *p_selected;
7713
if (ms->IsFocused)
7714
{
7715
ImGuiMultiSelectState* storage = ms->Storage;
7716
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7717
IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope");
7718
7719
// Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect().
7720
// This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper.
7721
// If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect()
7722
if (ms->LoopRequestSetAll != -1)
7723
selected = (ms->LoopRequestSetAll == 1);
7724
7725
// 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)
7726
// For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function)
7727
if (ms->IsKeyboardSetRange)
7728
{
7729
IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0);
7730
const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped.
7731
if (is_range_dst)
7732
ms->RangeDstPassedBy = true;
7733
if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst
7734
{
7735
storage->RangeSrcItem = item_data;
7736
storage->RangeSelected = selected ? 1 : 0;
7737
}
7738
const bool is_range_src = storage->RangeSrcItem == item_data;
7739
if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy)
7740
{
7741
// Apply range-select value to visible items
7742
IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1);
7743
selected = (storage->RangeSelected != 0);
7744
}
7745
else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7746
{
7747
// Clear other items
7748
selected = false;
7749
}
7750
}
7751
*p_selected = selected;
7752
}
7753
7754
// Alter button behavior flags
7755
// To handle drag and drop of multiple items we need to avoid clearing selection on click.
7756
// 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.
7757
if (p_button_flags != NULL)
7758
{
7759
ImGuiButtonFlags button_flags = *p_button_flags;
7760
button_flags |= ImGuiButtonFlags_NoHoveredOnFocus;
7761
if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease))
7762
button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease;
7763
else
7764
button_flags |= ImGuiButtonFlags_PressedOnClickRelease;
7765
*p_button_flags = button_flags;
7766
}
7767
}
7768
7769
// In charge of:
7770
// - Auto-select on navigation.
7771
// - Box-select toggle handling.
7772
// - Right-click handling.
7773
// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse.
7774
// - Record current selection state for RangeSrc
7775
// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite.
7776
void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed)
7777
{
7778
ImGuiContext& g = *GImGui;
7779
ImGuiWindow* window = g.CurrentWindow;
7780
7781
bool selected = *p_selected;
7782
bool pressed = *p_pressed;
7783
ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect;
7784
ImGuiMultiSelectState* storage = ms->Storage;
7785
if (pressed)
7786
ms->IsFocused = true;
7787
7788
bool hovered = false;
7789
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)
7790
hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
7791
if (!ms->IsFocused && !hovered)
7792
return;
7793
7794
ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData;
7795
7796
ImGuiMultiSelectFlags flags = ms->Flags;
7797
const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0;
7798
bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0;
7799
bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0;
7800
7801
bool apply_to_range_src = false;
7802
7803
if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7804
apply_to_range_src = true;
7805
if (ms->IsEndIO == false)
7806
{
7807
ms->IO.Requests.resize(0);
7808
ms->IsEndIO = true;
7809
}
7810
7811
// Auto-select as you navigate a list
7812
if (g.NavJustMovedToId == id)
7813
{
7814
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7815
{
7816
if (is_ctrl && is_shift)
7817
pressed = true;
7818
else if (!is_ctrl)
7819
selected = pressed = true;
7820
}
7821
else
7822
{
7823
// With NoAutoSelect, using Shift+keyboard performs a write/copy
7824
if (is_shift)
7825
pressed = true;
7826
else if (!is_ctrl)
7827
apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this
7828
}
7829
}
7830
7831
if (apply_to_range_src)
7832
{
7833
storage->RangeSrcItem = item_data;
7834
storage->RangeSelected = selected; // Will be updated at the end of this function anyway.
7835
}
7836
7837
// Box-select toggle handling
7838
if (ms->BoxSelectId != 0)
7839
if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId))
7840
{
7841
const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect);
7842
const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect);
7843
if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr))
7844
{
7845
if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce)
7846
{
7847
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)
7848
bs->IsStartedSetNavIdOnce = false;
7849
}
7850
else
7851
{
7852
selected = !selected;
7853
MultiSelectAddSetRange(ms, selected, +1, item_data, item_data);
7854
}
7855
storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1);
7856
}
7857
}
7858
7859
// Right-click handling.
7860
// FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816
7861
if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7862
{
7863
if (g.ActiveId != 0 && g.ActiveId != id)
7864
ClearActiveID();
7865
SetFocusID(id, window);
7866
if (!pressed && !selected)
7867
{
7868
pressed = true;
7869
is_ctrl = is_shift = false;
7870
}
7871
}
7872
7873
// Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected.
7874
// The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something
7875
// (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional?
7876
const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput);
7877
7878
// Alter selection
7879
if (pressed && (!enter_pressed || !selected))
7880
{
7881
// Box-select
7882
ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse;
7883
if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d))
7884
if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1)
7885
BoxSelectPreStartDrag(ms->BoxSelectId, item_data);
7886
7887
//----------------------------------------------------------------------------------------
7888
// ACTION | Begin | Pressed/Activated | End
7889
//----------------------------------------------------------------------------------------
7890
// Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1
7891
// Keys Navigated: Ctrl | n/a | n/a
7892
// Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7893
// Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst
7894
// Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1
7895
// Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1
7896
// Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1
7897
//----------------------------------------------------------------------------------------
7898
// Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1
7899
// Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1
7900
// Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1
7901
// Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst
7902
//----------------------------------------------------------------------------------------
7903
7904
if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0)
7905
{
7906
bool request_clear = false;
7907
if (is_singleselect)
7908
request_clear = true;
7909
else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl)
7910
request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true;
7911
else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl)
7912
request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again.
7913
if (request_clear)
7914
MultiSelectAddSetAll(ms, false);
7915
}
7916
7917
int range_direction;
7918
bool range_selected;
7919
if (is_shift && !is_singleselect)
7920
{
7921
//IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue);
7922
if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid)
7923
storage->RangeSrcItem = item_data;
7924
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7925
{
7926
// Shift+Arrow always select
7927
// Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected)
7928
range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7929
}
7930
else
7931
{
7932
// Shift+Arrow copy source selection state
7933
// Shift+Click always copy from target selection state
7934
if (ms->IsKeyboardSetRange)
7935
range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true;
7936
else
7937
range_selected = !selected;
7938
}
7939
range_direction = ms->RangeSrcPassedBy ? +1 : -1;
7940
}
7941
else
7942
{
7943
// Ctrl inverts selection, otherwise always select
7944
if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0)
7945
selected = is_ctrl ? !selected : true;
7946
else
7947
selected = !selected;
7948
storage->RangeSrcItem = item_data;
7949
range_selected = selected;
7950
range_direction = +1;
7951
}
7952
MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data);
7953
}
7954
7955
// Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect)
7956
if (storage->RangeSrcItem == item_data)
7957
storage->RangeSelected = selected ? 1 : 0;
7958
7959
// Update/store the selection state of focused item
7960
if (g.NavId == id)
7961
{
7962
storage->NavIdItem = item_data;
7963
storage->NavIdSelected = selected ? 1 : 0;
7964
}
7965
if (storage->NavIdItem == item_data)
7966
ms->NavIdPassedBy = true;
7967
ms->LastSubmittedItem = item_data;
7968
7969
*p_selected = selected;
7970
*p_pressed = pressed;
7971
}
7972
7973
void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected)
7974
{
7975
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid };
7976
ms->IO.Requests.resize(0); // Can always clear previous requests
7977
ms->IO.Requests.push_back(req); // Add new request
7978
}
7979
7980
void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item)
7981
{
7982
// Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges)
7983
if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0)
7984
{
7985
ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1];
7986
if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected)
7987
{
7988
prev->RangeLastItem = last_item;
7989
return;
7990
}
7991
}
7992
7993
ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item };
7994
ms->IO.Requests.push_back(req); // Add new request
7995
}
7996
7997
void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage)
7998
{
7999
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
8000
const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here.
8001
if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); }
8002
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*");
8003
if (!is_active) { PopStyleColor(); }
8004
if (!open)
8005
return;
8006
Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected);
8007
Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected);
8008
Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user
8009
TreePop();
8010
#else
8011
IM_UNUSED(storage);
8012
#endif
8013
}
8014
8015
//-------------------------------------------------------------------------
8016
// [SECTION] Widgets: Multi-Select helpers
8017
//-------------------------------------------------------------------------
8018
// - ImGuiSelectionBasicStorage
8019
// - ImGuiSelectionExternalStorage
8020
//-------------------------------------------------------------------------
8021
8022
ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage()
8023
{
8024
Size = 0;
8025
PreserveOrder = false;
8026
UserData = NULL;
8027
AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; };
8028
_SelectionOrder = 1; // Always >0
8029
}
8030
8031
void ImGuiSelectionBasicStorage::Clear()
8032
{
8033
Size = 0;
8034
_SelectionOrder = 1; // Always >0
8035
_Storage.Data.resize(0);
8036
}
8037
8038
void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r)
8039
{
8040
ImSwap(Size, r.Size);
8041
ImSwap(_SelectionOrder, r._SelectionOrder);
8042
_Storage.Data.swap(r._Storage.Data);
8043
}
8044
8045
bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const
8046
{
8047
return _Storage.GetInt(id, 0) != 0;
8048
}
8049
8050
static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs)
8051
{
8052
int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i;
8053
int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i;
8054
return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0);
8055
}
8056
8057
// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user.
8058
// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector<ImGuiID> instead of ImGuiStorage...)
8059
bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id)
8060
{
8061
ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it;
8062
ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size;
8063
if (PreserveOrder && it == NULL && it_end != NULL)
8064
ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt()
8065
if (it == NULL)
8066
it = _Storage.Data.Data;
8067
IM_ASSERT(it >= _Storage.Data.Data && it <= it_end);
8068
if (it != it_end)
8069
while (it->val_i == 0 && it < it_end)
8070
it++;
8071
const bool has_more = (it != it_end);
8072
*opaque_it = has_more ? (void**)(it + 1) : (void**)(it);
8073
*out_id = has_more ? it->key : 0;
8074
if (PreserveOrder && !has_more)
8075
_Storage.BuildSortByKey();
8076
return has_more;
8077
}
8078
8079
void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected)
8080
{
8081
int* p_int = _Storage.GetIntRef(id, 0);
8082
if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; }
8083
else if (!selected && *p_int != 0) { *p_int = 0; Size--; }
8084
}
8085
8086
// Optimized for batch edits (with same value of 'selected')
8087
static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order)
8088
{
8089
ImGuiStorage* storage = &selection->_Storage;
8090
ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id);
8091
const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id);
8092
if (selected == (is_contained && it->val_i != 0))
8093
return;
8094
if (selected && !is_contained)
8095
storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish()
8096
else if (is_contained)
8097
it->val_i = selected ? selection_order : 0; // Modify in-place.
8098
selection->Size += selected ? +1 : -1;
8099
}
8100
8101
static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends)
8102
{
8103
ImGuiStorage* storage = &selection->_Storage;
8104
if (selected && selection->Size != size_before_amends)
8105
storage->BuildSortByKey(); // When done selecting: sort everything
8106
}
8107
8108
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8109
// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen.
8110
// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem.
8111
// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection.
8112
// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform
8113
// a lookup in order to have some way to iterate/interpolate between two items.
8114
// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices
8115
// and constructing a view index <> object id/ptr data structure anyway.
8116
// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC.
8117
// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters.
8118
// The most simple implementation (using indices everywhere) would look like:
8119
// for (ImGuiSelectionRequest& req : ms_io->Requests)
8120
// {
8121
// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } }
8122
// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } }
8123
// }
8124
void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8125
{
8126
// For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional.
8127
// It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect().
8128
// Other scheme may handle SetAll differently.
8129
IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!");
8130
IM_ASSERT(AdapterIndexToStorageId != NULL);
8131
8132
// This is optimized/specialized to cope with very large selections (e.g. 100k+ items)
8133
// - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish().
8134
// - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass.
8135
// - 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.
8136
// - 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
8137
// left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.)
8138
// FIXME-OPT: For each block of consecutive SetRange request:
8139
// - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage.
8140
// - rewrite sorted storage a single time.
8141
for (ImGuiSelectionRequest& req : ms_io->Requests)
8142
{
8143
if (req.Type == ImGuiSelectionRequestType_SetAll)
8144
{
8145
Clear();
8146
if (req.Selected)
8147
{
8148
_Storage.Data.reserve(ms_io->ItemsCount);
8149
const int size_before_amends = _Storage.Data.Size;
8150
for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++)
8151
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder);
8152
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8153
}
8154
}
8155
else if (req.Type == ImGuiSelectionRequestType_SetRange)
8156
{
8157
const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1;
8158
//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);
8159
if (selection_changes == 1 || (selection_changes < Size / 100))
8160
{
8161
// Multiple sorted insertion + copy likely to be faster.
8162
// Technically we could do a single copy with a little more work (sort sequential SetRange requests)
8163
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8164
SetItemSelected(GetStorageIdFromIndex(idx), req.Selected);
8165
}
8166
else
8167
{
8168
// Append insertion + single sort likely be faster.
8169
// Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1
8170
const int size_before_amends = _Storage.Data.Size;
8171
int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0);
8172
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection)
8173
ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order);
8174
if (req.Selected)
8175
_SelectionOrder += selection_changes;
8176
ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends);
8177
}
8178
}
8179
}
8180
}
8181
8182
//-------------------------------------------------------------------------
8183
8184
ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage()
8185
{
8186
UserData = NULL;
8187
AdapterSetItemSelected = NULL;
8188
}
8189
8190
// Apply requests coming from BeginMultiSelect() and EndMultiSelect().
8191
// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage
8192
// This makes no assumption about underlying storage.
8193
void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io)
8194
{
8195
IM_ASSERT(AdapterSetItemSelected);
8196
for (ImGuiSelectionRequest& req : ms_io->Requests)
8197
{
8198
if (req.Type == ImGuiSelectionRequestType_SetAll)
8199
for (int idx = 0; idx < ms_io->ItemsCount; idx++)
8200
AdapterSetItemSelected(this, idx, req.Selected);
8201
if (req.Type == ImGuiSelectionRequestType_SetRange)
8202
for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++)
8203
AdapterSetItemSelected(this, idx, req.Selected);
8204
}
8205
}
8206
8207
//-------------------------------------------------------------------------
8208
// [SECTION] Widgets: ListBox
8209
//-------------------------------------------------------------------------
8210
// - BeginListBox()
8211
// - EndListBox()
8212
// - ListBox()
8213
//-------------------------------------------------------------------------
8214
8215
// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label.
8216
// This handle some subtleties with capturing info from the label, but for 99% uses it could essentially be rewritten as:
8217
// if (ImGui::BeginChild("...", ImVec2(ImGui::CalcItemWidth(), ImGui::GetTextLineHeight() * 7.5f), ImGuiChildFlags_FrameStyle))
8218
// { .... }
8219
// ImGui::EndChild();
8220
// ImGui::SameLine();
8221
// ImGui::AlignTextToFramePadding();
8222
// ImGui::Text("Label");
8223
// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty"
8224
// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.25 * item_height).
8225
bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg)
8226
{
8227
ImGuiContext& g = *GImGui;
8228
ImGuiWindow* window = GetCurrentWindow();
8229
if (window->SkipItems)
8230
return false;
8231
8232
const ImGuiStyle& style = g.Style;
8233
const ImGuiID id = GetID(label);
8234
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8235
8236
// Size default to hold ~7.25 items.
8237
// Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar.
8238
ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f));
8239
ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y));
8240
ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8241
ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
8242
g.NextItemData.ClearFlags();
8243
8244
if (!IsRectVisible(bb.Min, bb.Max))
8245
{
8246
ItemSize(bb.GetSize(), style.FramePadding.y);
8247
ItemAdd(bb, 0, &frame_bb);
8248
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8249
return false;
8250
}
8251
8252
// FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well.
8253
BeginGroup();
8254
if (label_size.x > 0.0f)
8255
{
8256
ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y);
8257
RenderText(label_pos, label);
8258
window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size);
8259
AlignTextToFramePadding();
8260
}
8261
8262
BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle);
8263
return true;
8264
}
8265
8266
void ImGui::EndListBox()
8267
{
8268
ImGuiContext& g = *GImGui;
8269
ImGuiWindow* window = g.CurrentWindow;
8270
IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?");
8271
IM_UNUSED(window);
8272
8273
EndChild();
8274
EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label
8275
}
8276
8277
bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items)
8278
{
8279
const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items);
8280
return value_changed;
8281
}
8282
8283
// This is merely a helper around BeginListBox(), EndListBox().
8284
// Considering using those directly to submit custom data or store selection differently.
8285
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)
8286
{
8287
ImGuiContext& g = *GImGui;
8288
8289
// Calculate size from "height_in_items"
8290
if (height_in_items < 0)
8291
height_in_items = ImMin(items_count, 7);
8292
float height_in_items_f = height_in_items + 0.25f;
8293
ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
8294
8295
if (!BeginListBox(label, size))
8296
return false;
8297
8298
// Assume all items have even height (= 1 line of text). If you need items of different height,
8299
// you can create a custom version of ListBox() in your code without using the clipper.
8300
bool value_changed = false;
8301
ImGuiListClipper clipper;
8302
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.
8303
clipper.IncludeItemByIndex(*current_item);
8304
while (clipper.Step())
8305
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
8306
{
8307
const char* item_text = getter(user_data, i);
8308
if (item_text == NULL)
8309
item_text = "*Unknown item*";
8310
8311
PushID(i);
8312
const bool item_selected = (i == *current_item);
8313
if (Selectable(item_text, item_selected))
8314
{
8315
*current_item = i;
8316
value_changed = true;
8317
}
8318
if (item_selected)
8319
SetItemDefaultFocus();
8320
PopID();
8321
}
8322
EndListBox();
8323
8324
if (value_changed)
8325
MarkItemEdited(g.LastItemData.ID);
8326
8327
return value_changed;
8328
}
8329
8330
//-------------------------------------------------------------------------
8331
// [SECTION] Widgets: PlotLines, PlotHistogram
8332
//-------------------------------------------------------------------------
8333
// - PlotEx() [Internal]
8334
// - PlotLines()
8335
// - PlotHistogram()
8336
//-------------------------------------------------------------------------
8337
// Plot/Graph widgets are not very good.
8338
// Consider writing your own, or using a third-party one, see:
8339
// - ImPlot https://github.com/epezent/implot
8340
// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions
8341
//-------------------------------------------------------------------------
8342
8343
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)
8344
{
8345
ImGuiContext& g = *GImGui;
8346
ImGuiWindow* window = GetCurrentWindow();
8347
if (window->SkipItems)
8348
return -1;
8349
8350
const ImGuiStyle& style = g.Style;
8351
const ImGuiID id = window->GetID(label);
8352
8353
const ImVec2 label_size = CalcTextSize(label, NULL, true);
8354
const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f);
8355
8356
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size);
8357
const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding);
8358
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));
8359
ItemSize(total_bb, style.FramePadding.y);
8360
if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav))
8361
return -1;
8362
bool hovered;
8363
ButtonBehavior(frame_bb, id, &hovered, NULL);
8364
8365
// Determine scale from values if not specified
8366
if (scale_min == FLT_MAX || scale_max == FLT_MAX)
8367
{
8368
float v_min = FLT_MAX;
8369
float v_max = -FLT_MAX;
8370
for (int i = 0; i < values_count; i++)
8371
{
8372
const float v = values_getter(data, i);
8373
if (v != v) // Ignore NaN values
8374
continue;
8375
v_min = ImMin(v_min, v);
8376
v_max = ImMax(v_max, v);
8377
}
8378
if (scale_min == FLT_MAX)
8379
scale_min = v_min;
8380
if (scale_max == FLT_MAX)
8381
scale_max = v_max;
8382
}
8383
8384
RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding);
8385
8386
const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1;
8387
int idx_hovered = -1;
8388
if (values_count >= values_count_min)
8389
{
8390
int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8391
int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0);
8392
8393
// Tooltip on hover
8394
if (hovered && inner_bb.Contains(g.IO.MousePos))
8395
{
8396
const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f);
8397
const int v_idx = (int)(t * item_count);
8398
IM_ASSERT(v_idx >= 0 && v_idx < values_count);
8399
8400
const float v0 = values_getter(data, (v_idx + values_offset) % values_count);
8401
const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count);
8402
if (plot_type == ImGuiPlotType_Lines)
8403
SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1);
8404
else if (plot_type == ImGuiPlotType_Histogram)
8405
SetTooltip("%d: %8.4g", v_idx, v0);
8406
idx_hovered = v_idx;
8407
}
8408
8409
const float t_step = 1.0f / (float)res_w;
8410
const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min));
8411
8412
float v0 = values_getter(data, (0 + values_offset) % values_count);
8413
float t0 = 0.0f;
8414
ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle
8415
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
8416
8417
const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram);
8418
const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered);
8419
8420
for (int n = 0; n < res_w; n++)
8421
{
8422
const float t1 = t0 + t_step;
8423
const int v1_idx = (int)(t0 * item_count + 0.5f);
8424
IM_ASSERT(v1_idx >= 0 && v1_idx < values_count);
8425
const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count);
8426
const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) );
8427
8428
// 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.
8429
ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0);
8430
ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t));
8431
if (plot_type == ImGuiPlotType_Lines)
8432
{
8433
window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8434
}
8435
else if (plot_type == ImGuiPlotType_Histogram)
8436
{
8437
if (pos1.x >= pos0.x + 2.0f)
8438
pos1.x -= 1.0f;
8439
window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base);
8440
}
8441
8442
t0 = t1;
8443
tp0 = tp1;
8444
}
8445
}
8446
8447
// Text overlay
8448
if (overlay_text)
8449
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));
8450
8451
if (label_size.x > 0.0f)
8452
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label);
8453
8454
// Return hovered index or -1 if none are hovered.
8455
// 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().
8456
return idx_hovered;
8457
}
8458
8459
struct ImGuiPlotArrayGetterData
8460
{
8461
const float* Values;
8462
int Stride;
8463
8464
ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; }
8465
};
8466
8467
static float Plot_ArrayGetter(void* data, int idx)
8468
{
8469
ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data;
8470
const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride);
8471
return v;
8472
}
8473
8474
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)
8475
{
8476
ImGuiPlotArrayGetterData data(values, stride);
8477
PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8478
}
8479
8480
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)
8481
{
8482
PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8483
}
8484
8485
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)
8486
{
8487
ImGuiPlotArrayGetterData data(values, stride);
8488
PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8489
}
8490
8491
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)
8492
{
8493
PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size);
8494
}
8495
8496
//-------------------------------------------------------------------------
8497
// [SECTION] Widgets: Value helpers
8498
// Those is not very useful, legacy API.
8499
//-------------------------------------------------------------------------
8500
// - Value()
8501
//-------------------------------------------------------------------------
8502
8503
void ImGui::Value(const char* prefix, bool b)
8504
{
8505
Text("%s: %s", prefix, (b ? "true" : "false"));
8506
}
8507
8508
void ImGui::Value(const char* prefix, int v)
8509
{
8510
Text("%s: %d", prefix, v);
8511
}
8512
8513
void ImGui::Value(const char* prefix, unsigned int v)
8514
{
8515
Text("%s: %d", prefix, v);
8516
}
8517
8518
void ImGui::Value(const char* prefix, float v, const char* float_format)
8519
{
8520
if (float_format)
8521
{
8522
char fmt[64];
8523
ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format);
8524
Text(fmt, prefix, v);
8525
}
8526
else
8527
{
8528
Text("%s: %.3f", prefix, v);
8529
}
8530
}
8531
8532
//-------------------------------------------------------------------------
8533
// [SECTION] MenuItem, BeginMenu, EndMenu, etc.
8534
//-------------------------------------------------------------------------
8535
// - ImGuiMenuColumns [Internal]
8536
// - BeginMenuBar()
8537
// - EndMenuBar()
8538
// - BeginMainMenuBar()
8539
// - EndMainMenuBar()
8540
// - BeginMenu()
8541
// - EndMenu()
8542
// - MenuItemEx() [Internal]
8543
// - MenuItem()
8544
//-------------------------------------------------------------------------
8545
8546
// Helpers for internal use
8547
void ImGuiMenuColumns::Update(float spacing, bool window_reappearing)
8548
{
8549
if (window_reappearing)
8550
memset(Widths, 0, sizeof(Widths));
8551
Spacing = (ImU16)spacing;
8552
CalcNextTotalWidth(true);
8553
memset(Widths, 0, sizeof(Widths));
8554
TotalWidth = NextTotalWidth;
8555
NextTotalWidth = 0;
8556
}
8557
8558
void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets)
8559
{
8560
ImU16 offset = 0;
8561
bool want_spacing = false;
8562
for (int i = 0; i < IM_ARRAYSIZE(Widths); i++)
8563
{
8564
ImU16 width = Widths[i];
8565
if (want_spacing && width > 0)
8566
offset += Spacing;
8567
want_spacing |= (width > 0);
8568
if (update_offsets)
8569
{
8570
if (i == 1) { OffsetLabel = offset; }
8571
if (i == 2) { OffsetShortcut = offset; }
8572
if (i == 3) { OffsetMark = offset; }
8573
}
8574
offset += width;
8575
}
8576
NextTotalWidth = offset;
8577
}
8578
8579
float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark)
8580
{
8581
Widths[0] = ImMax(Widths[0], (ImU16)w_icon);
8582
Widths[1] = ImMax(Widths[1], (ImU16)w_label);
8583
Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut);
8584
Widths[3] = ImMax(Widths[3], (ImU16)w_mark);
8585
CalcNextTotalWidth(false);
8586
return (float)ImMax(TotalWidth, NextTotalWidth);
8587
}
8588
8589
// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere..
8590
// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer.
8591
// 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.
8592
// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars.
8593
bool ImGui::BeginMenuBar()
8594
{
8595
ImGuiWindow* window = GetCurrentWindow();
8596
if (window->SkipItems)
8597
return false;
8598
if (!(window->Flags & ImGuiWindowFlags_MenuBar))
8599
return false;
8600
8601
IM_ASSERT(!window->DC.MenuBarAppending);
8602
BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8603
PushID("##menubar");
8604
8605
// 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.
8606
// 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.
8607
ImRect bar_rect = window->MenuBarRect();
8608
ImRect clip_rect(ImFloor(bar_rect.Min.x + window->WindowBorderSize), ImFloor(bar_rect.Min.y + window->WindowBorderSize), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize))), ImFloor(bar_rect.Max.y));
8609
clip_rect.ClipWith(window->OuterRectClipped);
8610
PushClipRect(clip_rect.Min, clip_rect.Max, false);
8611
8612
// 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).
8613
window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y);
8614
window->DC.LayoutType = ImGuiLayoutType_Horizontal;
8615
window->DC.IsSameLine = false;
8616
window->DC.NavLayerCurrent = ImGuiNavLayer_Menu;
8617
window->DC.MenuBarAppending = true;
8618
AlignTextToFramePadding();
8619
return true;
8620
}
8621
8622
void ImGui::EndMenuBar()
8623
{
8624
ImGuiWindow* window = GetCurrentWindow();
8625
if (window->SkipItems)
8626
return;
8627
ImGuiContext& g = *GImGui;
8628
8629
// Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings.
8630
if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu))
8631
{
8632
// Try to find out if the request is for one of our child menu
8633
ImGuiWindow* nav_earliest_child = g.NavWindow;
8634
while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu))
8635
nav_earliest_child = nav_earliest_child->ParentWindow;
8636
if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0)
8637
{
8638
// To do so we claim focus back, restore NavId and then process the movement request for yet another frame.
8639
// 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)
8640
const ImGuiNavLayer layer = ImGuiNavLayer_Menu;
8641
IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary)
8642
FocusWindow(window);
8643
SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]);
8644
// FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto?
8645
if (g.NavCursorVisible)
8646
{
8647
g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again
8648
g.NavCursorHideFrames = 2;
8649
}
8650
g.NavHighlightItemUnderNav = g.NavMousePosDirty = true;
8651
NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat
8652
}
8653
}
8654
8655
IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'"
8656
IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar);
8657
IM_ASSERT(window->DC.MenuBarAppending);
8658
PopClipRect();
8659
PopID();
8660
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.
8661
8662
// FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here.
8663
ImGuiGroupData& group_data = g.GroupStack.back();
8664
group_data.EmitItem = false;
8665
ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos;
8666
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent.
8667
EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore
8668
window->DC.LayoutType = ImGuiLayoutType_Vertical;
8669
window->DC.IsSameLine = false;
8670
window->DC.NavLayerCurrent = ImGuiNavLayer_Main;
8671
window->DC.MenuBarAppending = false;
8672
window->DC.CursorMaxPos = restore_cursor_max_pos;
8673
}
8674
8675
// Important: calling order matters!
8676
// FIXME: Somehow overlapping with docking tech.
8677
// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts)
8678
bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags)
8679
{
8680
IM_ASSERT(dir != ImGuiDir_None);
8681
8682
ImGuiWindow* bar_window = FindWindowByName(name);
8683
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport());
8684
if (bar_window == NULL || bar_window->BeginCount == 0)
8685
{
8686
// Calculate and set window size/position
8687
ImRect avail_rect = viewport->GetBuildWorkRect();
8688
ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X;
8689
ImVec2 pos = avail_rect.Min;
8690
if (dir == ImGuiDir_Right || dir == ImGuiDir_Down)
8691
pos[axis] = avail_rect.Max[axis] - axis_size;
8692
ImVec2 size = avail_rect.GetSize();
8693
size[axis] = axis_size;
8694
SetNextWindowPos(pos);
8695
SetNextWindowSize(size);
8696
8697
// Report our size into work area (for next frame) using actual window size
8698
if (dir == ImGuiDir_Up || dir == ImGuiDir_Left)
8699
viewport->BuildWorkInsetMin[axis] += axis_size;
8700
else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right)
8701
viewport->BuildWorkInsetMax[axis] += axis_size;
8702
}
8703
8704
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
8705
SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set.
8706
PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
8707
PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint
8708
bool is_open = Begin(name, NULL, window_flags);
8709
PopStyleVar(2);
8710
8711
return is_open;
8712
}
8713
8714
bool ImGui::BeginMainMenuBar()
8715
{
8716
ImGuiContext& g = *GImGui;
8717
ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport();
8718
8719
// Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change
8720
SetCurrentViewport(NULL, viewport);
8721
8722
// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set.
8723
// FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea?
8724
// FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings.
8725
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f));
8726
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
8727
float height = GetFrameHeight();
8728
bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags);
8729
g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f);
8730
8731
if (is_open)
8732
BeginMenuBar();
8733
else
8734
End();
8735
return is_open;
8736
}
8737
8738
void ImGui::EndMainMenuBar()
8739
{
8740
EndMenuBar();
8741
8742
// When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window
8743
// FIXME: With this strategy we won't be able to restore a NULL focus.
8744
ImGuiContext& g = *GImGui;
8745
if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest)
8746
FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild);
8747
8748
End();
8749
}
8750
8751
static bool IsRootOfOpenMenuSet()
8752
{
8753
ImGuiContext& g = *GImGui;
8754
ImGuiWindow* window = g.CurrentWindow;
8755
if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu))
8756
return false;
8757
8758
// Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others
8759
// (e.g. inside menu bar vs loose menu items) based on parent ID.
8760
// This would however prevent the use of e.g. PushID() user code submitting menus.
8761
// Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag,
8762
// making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects.
8763
// Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup
8764
// doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu.
8765
// 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.
8766
// 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
8767
// 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
8768
// it likely won't be a problem anyone runs into.
8769
const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size];
8770
if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer)
8771
return false;
8772
return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false);
8773
}
8774
8775
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
8776
{
8777
ImGuiWindow* window = GetCurrentWindow();
8778
if (window->SkipItems)
8779
return false;
8780
8781
ImGuiContext& g = *GImGui;
8782
const ImGuiStyle& style = g.Style;
8783
const ImGuiID id = window->GetID(label);
8784
bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);
8785
8786
// 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)
8787
// 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.
8788
ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus;
8789
if (window->Flags & ImGuiWindowFlags_ChildMenu)
8790
window_flags |= ImGuiWindowFlags_ChildWindow;
8791
8792
// If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin().
8793
// 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.
8794
// If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used.
8795
if (g.MenusIdSubmittedThisFrame.contains(id))
8796
{
8797
if (menu_is_open)
8798
menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8799
else
8800
g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values
8801
return menu_is_open;
8802
}
8803
8804
// Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu
8805
g.MenusIdSubmittedThisFrame.push_back(id);
8806
8807
ImVec2 label_size = CalcTextSize(label, NULL, true);
8808
8809
// 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)
8810
// This is only done for items for the menu set and not the full parent window.
8811
const bool menuset_is_open = IsRootOfOpenMenuSet();
8812
if (menuset_is_open)
8813
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
8814
8815
// The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu,
8816
// However the final position is going to be different! It is chosen by FindBestWindowPosForPopup().
8817
// e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering.
8818
ImVec2 popup_pos, pos = window->DC.CursorPos;
8819
PushID(label);
8820
if (!enabled)
8821
BeginDisabled();
8822
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
8823
bool pressed;
8824
8825
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
8826
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups;
8827
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
8828
{
8829
// Menu inside an horizontal menu bar
8830
// Selectable extend their highlight by half ItemSpacing in each direction.
8831
// For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin()
8832
popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight);
8833
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
8834
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
8835
float w = label_size.x;
8836
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8837
pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));
8838
LogSetNextTextDecoration("[", "]");
8839
RenderText(text_pos, label);
8840
PopStyleVar();
8841
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().
8842
}
8843
else
8844
{
8845
// Menu inside a regular/vertical menu
8846
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
8847
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
8848
popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y);
8849
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
8850
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
8851
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame
8852
float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
8853
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
8854
pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
8855
LogSetNextTextDecoration("", ">");
8856
RenderText(text_pos, label);
8857
if (icon_w > 0.0f)
8858
RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
8859
RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right);
8860
}
8861
if (!enabled)
8862
EndDisabled();
8863
8864
const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav;
8865
if (menuset_is_open)
8866
PopItemFlag();
8867
8868
bool want_open = false;
8869
bool want_open_nav_init = false;
8870
bool want_close = false;
8871
if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu))
8872
{
8873
// Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu
8874
// Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive.
8875
bool moving_toward_child_menu = false;
8876
ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below)
8877
ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL;
8878
if (g.HoveredWindow == window && child_menu_window != NULL)
8879
{
8880
const float ref_unit = g.FontSize; // FIXME-DPI
8881
const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f;
8882
const ImRect next_window_rect = child_menu_window->Rect();
8883
ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta);
8884
ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR();
8885
ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR();
8886
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.
8887
ta.x += child_dir * -0.5f;
8888
tb.x += child_dir * ref_unit;
8889
tc.x += child_dir * ref_unit;
8890
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
8891
tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f);
8892
moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos);
8893
//GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG]
8894
}
8895
8896
// 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)
8897
// 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.
8898
// (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.)
8899
if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0)
8900
want_close = true;
8901
8902
// Open
8903
// (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test)
8904
if (!menu_is_open && pressed) // Click/activate to open
8905
want_open = true;
8906
else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open
8907
want_open = true;
8908
else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback)
8909
want_open = true;
8910
if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open
8911
{
8912
want_open = want_open_nav_init = true;
8913
NavMoveRequestCancel();
8914
SetNavCursorVisibleAfterMove();
8915
}
8916
}
8917
else
8918
{
8919
// Menu bar
8920
if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it
8921
{
8922
want_close = true;
8923
want_open = menu_is_open = false;
8924
}
8925
else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others
8926
{
8927
want_open = true;
8928
}
8929
else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open
8930
{
8931
want_open = true;
8932
NavMoveRequestCancel();
8933
}
8934
}
8935
8936
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.. }'
8937
want_close = true;
8938
if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None))
8939
ClosePopupToLevel(g.BeginPopupStack.Size, true);
8940
8941
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0));
8942
PopID();
8943
8944
if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size)
8945
{
8946
// 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.
8947
OpenPopup(label);
8948
}
8949
else if (want_open)
8950
{
8951
menu_is_open = true;
8952
OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0));
8953
}
8954
8955
if (menu_is_open)
8956
{
8957
ImGuiLastItemData last_item_in_parent = g.LastItemData;
8958
SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos.
8959
PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding
8960
menu_is_open = BeginPopupEx(id, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
8961
PopStyleVar();
8962
if (menu_is_open)
8963
{
8964
// Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do:
8965
// Perform an init request in the case the popup was already open (via a previous mouse hover)
8966
if (want_open && want_open_nav_init && !g.NavInitRequest)
8967
{
8968
FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal);
8969
NavInitWindow(g.CurrentWindow, false);
8970
}
8971
8972
// Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu()
8973
// (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck)
8974
g.LastItemData = last_item_in_parent;
8975
if (g.HoveredWindow == window)
8976
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
8977
}
8978
}
8979
else
8980
{
8981
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
8982
}
8983
8984
return menu_is_open;
8985
}
8986
8987
bool ImGui::BeginMenu(const char* label, bool enabled)
8988
{
8989
return BeginMenuEx(label, NULL, enabled);
8990
}
8991
8992
void ImGui::EndMenu()
8993
{
8994
// Nav: When a left move request our menu failed, close ourselves.
8995
ImGuiContext& g = *GImGui;
8996
ImGuiWindow* window = g.CurrentWindow;
8997
IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls
8998
ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert.
8999
if (window->BeginCount == window->BeginCountPreviousFrame)
9000
if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet())
9001
if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical)
9002
{
9003
ClosePopupToLevel(g.BeginPopupStack.Size - 1, true);
9004
NavMoveRequestCancel();
9005
}
9006
9007
EndPopup();
9008
}
9009
9010
bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled)
9011
{
9012
ImGuiWindow* window = GetCurrentWindow();
9013
if (window->SkipItems)
9014
return false;
9015
9016
ImGuiContext& g = *GImGui;
9017
ImGuiStyle& style = g.Style;
9018
ImVec2 pos = window->DC.CursorPos;
9019
ImVec2 label_size = CalcTextSize(label, NULL, true);
9020
9021
// See BeginMenuEx() for comments about this.
9022
const bool menuset_is_open = IsRootOfOpenMenuSet();
9023
if (menuset_is_open)
9024
PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true);
9025
9026
// We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73),
9027
// 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.
9028
bool pressed;
9029
PushID(label);
9030
if (!enabled)
9031
BeginDisabled();
9032
9033
// We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another.
9034
const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover;
9035
const ImGuiMenuColumns* offsets = &window->DC.MenuColumns;
9036
if (window->DC.LayoutType == ImGuiLayoutType_Horizontal)
9037
{
9038
// Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful
9039
// Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark.
9040
float w = label_size.x;
9041
window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f);
9042
ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
9043
PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f);
9044
pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f));
9045
PopStyleVar();
9046
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9047
RenderText(text_pos, label);
9048
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().
9049
}
9050
else
9051
{
9052
// Menu item inside a vertical menu
9053
// (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f.
9054
// Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system.
9055
float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f;
9056
float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f;
9057
float checkmark_w = IM_TRUNC(g.FontSize * 1.20f);
9058
float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame
9059
float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w);
9060
pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y));
9061
if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)
9062
{
9063
RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label);
9064
if (icon_w > 0.0f)
9065
RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon);
9066
if (shortcut_w > 0.0f)
9067
{
9068
PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]);
9069
LogSetNextTextDecoration("(", ")");
9070
RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false);
9071
PopStyleColor();
9072
}
9073
if (selected)
9074
RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f);
9075
}
9076
}
9077
IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0));
9078
if (!enabled)
9079
EndDisabled();
9080
PopID();
9081
if (menuset_is_open)
9082
PopItemFlag();
9083
9084
return pressed;
9085
}
9086
9087
bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled)
9088
{
9089
return MenuItemEx(label, NULL, shortcut, selected, enabled);
9090
}
9091
9092
bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled)
9093
{
9094
if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled))
9095
{
9096
if (p_selected)
9097
*p_selected = !*p_selected;
9098
return true;
9099
}
9100
return false;
9101
}
9102
9103
//-------------------------------------------------------------------------
9104
// [SECTION] Widgets: BeginTabBar, EndTabBar, etc.
9105
//-------------------------------------------------------------------------
9106
// - BeginTabBar()
9107
// - BeginTabBarEx() [Internal]
9108
// - EndTabBar()
9109
// - TabBarLayout() [Internal]
9110
// - TabBarCalcTabID() [Internal]
9111
// - TabBarCalcMaxTabWidth() [Internal]
9112
// - TabBarFindTabById() [Internal]
9113
// - TabBarFindTabByOrder() [Internal]
9114
// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal]
9115
// - TabBarGetCurrentTab() [Internal]
9116
// - TabBarGetTabName() [Internal]
9117
// - TabBarAddTab() [Internal]
9118
// - TabBarRemoveTab() [Internal]
9119
// - TabBarCloseTab() [Internal]
9120
// - TabBarScrollClamp() [Internal]
9121
// - TabBarScrollToTab() [Internal]
9122
// - TabBarQueueFocus() [Internal]
9123
// - TabBarQueueReorder() [Internal]
9124
// - TabBarProcessReorderFromMousePos() [Internal]
9125
// - TabBarProcessReorder() [Internal]
9126
// - TabBarScrollingButtons() [Internal]
9127
// - TabBarTabListPopupButton() [Internal]
9128
//-------------------------------------------------------------------------
9129
9130
struct ImGuiTabBarSection
9131
{
9132
int TabCount; // Number of tabs in this section.
9133
float Width; // Sum of width of tabs in this section (after shrinking down)
9134
float Spacing; // Horizontal spacing at the end of the section.
9135
9136
ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); }
9137
};
9138
9139
namespace ImGui
9140
{
9141
static void TabBarLayout(ImGuiTabBar* tab_bar);
9142
static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window);
9143
static float TabBarCalcMaxTabWidth();
9144
static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling);
9145
static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections);
9146
static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar);
9147
static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar);
9148
}
9149
9150
ImGuiTabBar::ImGuiTabBar()
9151
{
9152
memset(this, 0, sizeof(*this));
9153
CurrFrameVisible = PrevFrameVisible = -1;
9154
LastTabItemIdx = -1;
9155
}
9156
9157
static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab)
9158
{
9159
return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1;
9160
}
9161
9162
static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs)
9163
{
9164
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9165
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9166
const int a_section = TabItemGetSectionIdx(a);
9167
const int b_section = TabItemGetSectionIdx(b);
9168
if (a_section != b_section)
9169
return a_section - b_section;
9170
return (int)(a->IndexDuringLayout - b->IndexDuringLayout);
9171
}
9172
9173
static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
9174
{
9175
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
9176
const ImGuiTabItem* b = (const ImGuiTabItem*)rhs;
9177
return (int)(a->BeginOrder - b->BeginOrder);
9178
}
9179
9180
static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref)
9181
{
9182
ImGuiContext& g = *GImGui;
9183
return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index);
9184
}
9185
9186
static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar)
9187
{
9188
ImGuiContext& g = *GImGui;
9189
if (g.TabBars.Contains(tab_bar))
9190
return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar));
9191
return ImGuiPtrOrIndex(tab_bar);
9192
}
9193
9194
bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags)
9195
{
9196
ImGuiContext& g = *GImGui;
9197
ImGuiWindow* window = g.CurrentWindow;
9198
if (window->SkipItems)
9199
return false;
9200
9201
ImGuiID id = window->GetID(str_id);
9202
ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id);
9203
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);
9204
tab_bar->ID = id;
9205
tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f);
9206
tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f);
9207
//if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false))
9208
flags |= ImGuiTabBarFlags_IsFocused;
9209
return BeginTabBarEx(tab_bar, tab_bar_bb, flags);
9210
}
9211
9212
bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags)
9213
{
9214
ImGuiContext& g = *GImGui;
9215
ImGuiWindow* window = g.CurrentWindow;
9216
if (window->SkipItems)
9217
return false;
9218
9219
IM_ASSERT(tab_bar->ID != 0);
9220
if ((flags & ImGuiTabBarFlags_DockNode) == 0)
9221
PushOverrideID(tab_bar->ID);
9222
9223
// Add to stack
9224
g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar));
9225
g.CurrentTabBar = tab_bar;
9226
tab_bar->Window = window;
9227
9228
// Append with multiple BeginTabBar()/EndTabBar() pairs.
9229
tab_bar->BackupCursorPos = window->DC.CursorPos;
9230
if (tab_bar->CurrFrameVisible == g.FrameCount)
9231
{
9232
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9233
tab_bar->BeginCount++;
9234
return true;
9235
}
9236
9237
// Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable
9238
if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable)))
9239
if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid
9240
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder);
9241
tab_bar->TabsAddedNew = false;
9242
9243
// Flags
9244
if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0)
9245
flags |= ImGuiTabBarFlags_FittingPolicyDefault_;
9246
9247
tab_bar->Flags = flags;
9248
tab_bar->BarRect = tab_bar_bb;
9249
tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab()
9250
tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible;
9251
tab_bar->CurrFrameVisible = g.FrameCount;
9252
tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight;
9253
tab_bar->CurrTabsContentsHeight = 0.0f;
9254
tab_bar->ItemSpacingY = g.Style.ItemSpacing.y;
9255
tab_bar->FramePadding = g.Style.FramePadding;
9256
tab_bar->TabsActiveCount = 0;
9257
tab_bar->LastTabItemIdx = -1;
9258
tab_bar->BeginCount = 1;
9259
9260
// Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap
9261
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY);
9262
9263
// Draw separator
9264
// (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable)
9265
const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected);
9266
if (g.Style.TabBarBorderSize > 0.0f)
9267
{
9268
const float y = tab_bar->BarRect.Max.y;
9269
window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col);
9270
}
9271
return true;
9272
}
9273
9274
void ImGui::EndTabBar()
9275
{
9276
ImGuiContext& g = *GImGui;
9277
ImGuiWindow* window = g.CurrentWindow;
9278
if (window->SkipItems)
9279
return;
9280
9281
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9282
if (tab_bar == NULL)
9283
{
9284
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!");
9285
return;
9286
}
9287
9288
// Fallback in case no TabItem have been submitted
9289
if (tab_bar->WantLayout)
9290
TabBarLayout(tab_bar);
9291
9292
// Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed().
9293
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
9294
if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing)
9295
{
9296
tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight);
9297
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight;
9298
}
9299
else
9300
{
9301
window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight;
9302
}
9303
if (tab_bar->BeginCount > 1)
9304
window->DC.CursorPos = tab_bar->BackupCursorPos;
9305
9306
tab_bar->LastTabItemIdx = -1;
9307
if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0)
9308
PopID();
9309
9310
g.CurrentTabBarStack.pop_back();
9311
g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back());
9312
}
9313
9314
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9315
static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections)
9316
{
9317
return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing;
9318
}
9319
9320
// This is called only once a frame before by the first call to ItemTab()
9321
// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions.
9322
static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
9323
{
9324
ImGuiContext& g = *GImGui;
9325
tab_bar->WantLayout = false;
9326
9327
// Garbage collect by compacting list
9328
// Detect if we need to sort out tab list (e.g. in rare case where a tab changed section)
9329
int tab_dst_n = 0;
9330
bool need_sort_by_section = false;
9331
ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing
9332
for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
9333
{
9334
ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n];
9335
if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose)
9336
{
9337
// Remove tab
9338
if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; }
9339
if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; }
9340
if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; }
9341
continue;
9342
}
9343
if (tab_dst_n != tab_src_n)
9344
tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n];
9345
9346
tab = &tab_bar->Tabs[tab_dst_n];
9347
tab->IndexDuringLayout = (ImS16)tab_dst_n;
9348
9349
// We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another)
9350
int curr_tab_section_n = TabItemGetSectionIdx(tab);
9351
if (tab_dst_n > 0)
9352
{
9353
ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1];
9354
int prev_tab_section_n = TabItemGetSectionIdx(prev_tab);
9355
if (curr_tab_section_n == 0 && prev_tab_section_n != 0)
9356
need_sort_by_section = true;
9357
if (prev_tab_section_n == 2 && curr_tab_section_n != 2)
9358
need_sort_by_section = true;
9359
}
9360
9361
sections[curr_tab_section_n].TabCount++;
9362
tab_dst_n++;
9363
}
9364
if (tab_bar->Tabs.Size != tab_dst_n)
9365
tab_bar->Tabs.resize(tab_dst_n);
9366
9367
if (need_sort_by_section)
9368
ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection);
9369
9370
// Calculate spacing between sections
9371
sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9372
sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f;
9373
9374
// Setup next selected tab
9375
ImGuiID scroll_to_tab_id = 0;
9376
if (tab_bar->NextSelectedTabId)
9377
{
9378
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId;
9379
tab_bar->NextSelectedTabId = 0;
9380
scroll_to_tab_id = tab_bar->SelectedTabId;
9381
}
9382
9383
// Process order change request (we could probably process it when requested but it's just saner to do it in a single spot).
9384
if (tab_bar->ReorderRequestTabId != 0)
9385
{
9386
if (TabBarProcessReorder(tab_bar))
9387
if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId)
9388
scroll_to_tab_id = tab_bar->ReorderRequestTabId;
9389
tab_bar->ReorderRequestTabId = 0;
9390
}
9391
9392
// Tab List Popup (will alter tab_bar->BarRect and therefore the available width!)
9393
const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
9394
if (tab_list_popup_button)
9395
if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x!
9396
scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
9397
9398
// Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central
9399
// (whereas our tabs are stored as: leading, central, trailing)
9400
int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
9401
g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size);
9402
9403
// Compute ideal tabs widths + store them into shrink buffer
9404
ImGuiTabItem* most_recently_selected_tab = NULL;
9405
int curr_section_n = -1;
9406
bool found_selected_tab_id = false;
9407
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9408
{
9409
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9410
IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
9411
9412
if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button))
9413
most_recently_selected_tab = tab;
9414
if (tab->ID == tab_bar->SelectedTabId)
9415
found_selected_tab_id = true;
9416
if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID)
9417
scroll_to_tab_id = tab->ID;
9418
9419
// Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar.
9420
// Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet,
9421
// and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window.
9422
const char* tab_name = TabBarGetTabName(tab_bar, tab);
9423
const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
9424
tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x;
9425
9426
int section_n = TabItemGetSectionIdx(tab);
9427
ImGuiTabBarSection* section = &sections[section_n];
9428
section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f);
9429
curr_section_n = section_n;
9430
9431
// Store data so we can build an array sorted by width if we need to shrink tabs down
9432
IM_MSVC_WARNING_SUPPRESS(6385);
9433
ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++];
9434
shrink_width_item->Index = tab_n;
9435
shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth;
9436
tab->Width = ImMax(tab->ContentWidth, 1.0f);
9437
}
9438
9439
// Compute total ideal width (used for e.g. auto-resizing a window)
9440
tab_bar->WidthAllTabsIdeal = 0.0f;
9441
for (int section_n = 0; section_n < 3; section_n++)
9442
tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing;
9443
9444
// Horizontal scrolling buttons
9445
// (note that TabBarScrollButtons() will alter BarRect.Max.x)
9446
if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll))
9447
if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar))
9448
{
9449
scroll_to_tab_id = scroll_and_select_tab->ID;
9450
if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0)
9451
tab_bar->SelectedTabId = scroll_to_tab_id;
9452
}
9453
9454
// Shrink widths if full tabs don't fit in their allocated space
9455
float section_0_w = sections[0].Width + sections[0].Spacing;
9456
float section_1_w = sections[1].Width + sections[1].Spacing;
9457
float section_2_w = sections[2].Width + sections[2].Spacing;
9458
bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth();
9459
float width_excess;
9460
if (central_section_is_visible)
9461
width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section
9462
else
9463
width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section
9464
9465
// With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore
9466
if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible))
9467
{
9468
int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount);
9469
int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0);
9470
ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess);
9471
9472
// Apply shrunk values into tabs and sections
9473
for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++)
9474
{
9475
ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index];
9476
float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width);
9477
if (shrinked_width < 0.0f)
9478
continue;
9479
9480
shrinked_width = ImMax(1.0f, shrinked_width);
9481
int section_n = TabItemGetSectionIdx(tab);
9482
sections[section_n].Width -= (tab->Width - shrinked_width);
9483
tab->Width = shrinked_width;
9484
}
9485
}
9486
9487
// Layout all active tabs
9488
int section_tab_index = 0;
9489
float tab_offset = 0.0f;
9490
tab_bar->WidthAllTabs = 0.0f;
9491
for (int section_n = 0; section_n < 3; section_n++)
9492
{
9493
ImGuiTabBarSection* section = &sections[section_n];
9494
if (section_n == 2)
9495
tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset);
9496
9497
for (int tab_n = 0; tab_n < section->TabCount; tab_n++)
9498
{
9499
ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n];
9500
tab->Offset = tab_offset;
9501
tab->NameOffset = -1;
9502
tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
9503
}
9504
tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f);
9505
tab_offset += section->Spacing;
9506
section_tab_index += section->TabCount;
9507
}
9508
9509
// Clear name buffers
9510
tab_bar->TabsNames.Buf.resize(0);
9511
9512
// If we have lost the selected tab, select the next most recently active one
9513
if (found_selected_tab_id == false)
9514
tab_bar->SelectedTabId = 0;
9515
if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL)
9516
scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID;
9517
9518
// Lock in visible tab
9519
tab_bar->VisibleTabId = tab_bar->SelectedTabId;
9520
tab_bar->VisibleTabWasSubmitted = false;
9521
9522
// CTRL+TAB can override visible tab temporarily
9523
if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar)
9524
tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId;
9525
9526
// Apply request requests
9527
if (scroll_to_tab_id != 0)
9528
TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections);
9529
else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow))
9530
{
9531
const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH;
9532
const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX;
9533
if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f)
9534
{
9535
const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f;
9536
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9537
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step);
9538
}
9539
SetKeyOwner(wheel_key, tab_bar->ID);
9540
}
9541
9542
// Update scrolling
9543
tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim);
9544
tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget);
9545
if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget)
9546
{
9547
// Scrolling speed adjust itself so we can always reach our target in 1/3 seconds.
9548
// Teleport if we are aiming far off the visible line
9549
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize);
9550
tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f);
9551
const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize);
9552
tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed);
9553
}
9554
else
9555
{
9556
tab_bar->ScrollingSpeed = 0.0f;
9557
}
9558
tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing;
9559
tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing;
9560
9561
// Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame)
9562
ImGuiWindow* window = g.CurrentWindow;
9563
window->DC.CursorPos = tab_bar->BarRect.Min;
9564
ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y);
9565
window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal);
9566
}
9567
9568
// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack.
9569
static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window)
9570
{
9571
if (docked_window != NULL)
9572
{
9573
IM_UNUSED(tab_bar);
9574
IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
9575
ImGuiID id = docked_window->TabId;
9576
KeepAliveID(id);
9577
return id;
9578
}
9579
else
9580
{
9581
ImGuiWindow* window = GImGui->CurrentWindow;
9582
return window->GetID(label);
9583
}
9584
}
9585
9586
static float ImGui::TabBarCalcMaxTabWidth()
9587
{
9588
ImGuiContext& g = *GImGui;
9589
return g.FontSize * 20.0f;
9590
}
9591
9592
ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9593
{
9594
if (tab_id != 0)
9595
for (int n = 0; n < tab_bar->Tabs.Size; n++)
9596
if (tab_bar->Tabs[n].ID == tab_id)
9597
return &tab_bar->Tabs[n];
9598
return NULL;
9599
}
9600
9601
// Order = visible order, not submission order! (which is tab->BeginOrder)
9602
ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order)
9603
{
9604
if (order < 0 || order >= tab_bar->Tabs.Size)
9605
return NULL;
9606
return &tab_bar->Tabs[order];
9607
}
9608
9609
// FIXME: See references to #2304 in TODO.txt
9610
ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar)
9611
{
9612
ImGuiTabItem* most_recently_selected_tab = NULL;
9613
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9614
{
9615
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9616
if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
9617
if (tab->Window && tab->Window->WasActive)
9618
most_recently_selected_tab = tab;
9619
}
9620
return most_recently_selected_tab;
9621
}
9622
9623
ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar)
9624
{
9625
if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size)
9626
return NULL;
9627
return &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9628
}
9629
9630
const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9631
{
9632
if (tab->Window)
9633
return tab->Window->Name;
9634
if (tab->NameOffset == -1)
9635
return "N/A";
9636
IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size);
9637
return tab_bar->TabsNames.Buf.Data + tab->NameOffset;
9638
}
9639
9640
// The purpose of this call is to register tab in advance so we can control their order at the time they appear.
9641
// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function.
9642
void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window)
9643
{
9644
ImGuiContext& g = *GImGui;
9645
IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL);
9646
IM_ASSERT(g.CurrentTabBar != tab_bar); // Can't work while the tab bar is active as our tab doesn't have an X offset yet, in theory we could/should test something like (tab_bar->CurrFrameVisible < g.FrameCount) but we'd need to solve why triggers the commented early-out assert in BeginTabBarEx() (probably dock node going from implicit to explicit in same frame)
9647
9648
if (!window->HasCloseButton)
9649
tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation.
9650
9651
ImGuiTabItem new_tab;
9652
new_tab.ID = window->TabId;
9653
new_tab.Flags = tab_flags;
9654
new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab
9655
if (new_tab.LastFrameVisible == -1)
9656
new_tab.LastFrameVisible = g.FrameCount - 1;
9657
new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission
9658
tab_bar->Tabs.push_back(new_tab);
9659
}
9660
9661
// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless.
9662
void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id)
9663
{
9664
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
9665
tab_bar->Tabs.erase(tab);
9666
if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; }
9667
if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; }
9668
if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; }
9669
}
9670
9671
// Called on manual closure attempt
9672
void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9673
{
9674
if (tab->Flags & ImGuiTabItemFlags_Button)
9675
return; // A button appended with TabItemButton().
9676
9677
if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0)
9678
{
9679
// This will remove a frame of lag for selecting another tab on closure.
9680
// 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
9681
tab->WantClose = true;
9682
if (tab_bar->VisibleTabId == tab->ID)
9683
{
9684
tab->LastFrameVisible = -1;
9685
tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0;
9686
}
9687
}
9688
else
9689
{
9690
// Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup)
9691
if (tab_bar->VisibleTabId != tab->ID)
9692
TabBarQueueFocus(tab_bar, tab);
9693
}
9694
}
9695
9696
static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling)
9697
{
9698
scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
9699
return ImMax(scrolling, 0.0f);
9700
}
9701
9702
// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys
9703
static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections)
9704
{
9705
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id);
9706
if (tab == NULL)
9707
return;
9708
if (tab->Flags & ImGuiTabItemFlags_SectionMask_)
9709
return;
9710
9711
ImGuiContext& g = *GImGui;
9712
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)
9713
int order = TabBarGetTabOrder(tab_bar, tab);
9714
9715
// Scrolling happens only in the central section (leading/trailing sections are not scrolling)
9716
float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections);
9717
9718
// We make all tabs positions all relative Sections[0].Width to make code simpler
9719
float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
9720
float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f);
9721
tab_bar->ScrollingTargetDistToVisibility = 0.0f;
9722
if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width))
9723
{
9724
// Scroll to the left
9725
tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f);
9726
tab_bar->ScrollingTarget = tab_x1;
9727
}
9728
else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width)
9729
{
9730
// Scroll to the right
9731
tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f);
9732
tab_bar->ScrollingTarget = tab_x2 - scrollable_width;
9733
}
9734
}
9735
9736
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
9737
{
9738
tab_bar->NextSelectedTabId = tab->ID;
9739
}
9740
9741
void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name)
9742
{
9743
IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars
9744
ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL);
9745
tab_bar->NextSelectedTabId = tab_id;
9746
}
9747
9748
void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset)
9749
{
9750
IM_ASSERT(offset != 0);
9751
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9752
tab_bar->ReorderRequestTabId = tab->ID;
9753
tab_bar->ReorderRequestOffset = (ImS16)offset;
9754
}
9755
9756
void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos)
9757
{
9758
ImGuiContext& g = *GImGui;
9759
IM_ASSERT(tab_bar->ReorderRequestTabId == 0);
9760
if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0)
9761
return;
9762
9763
const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
9764
const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0);
9765
9766
// Count number of contiguous tabs we are crossing over
9767
const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1;
9768
const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab);
9769
int dst_idx = src_idx;
9770
for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir)
9771
{
9772
// Reordered tabs must share the same section
9773
const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i];
9774
if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder)
9775
break;
9776
if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_))
9777
break;
9778
dst_idx = i;
9779
9780
// Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered.
9781
const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x;
9782
const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x;
9783
//GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255));
9784
if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2))
9785
break;
9786
}
9787
9788
if (dst_idx != src_idx)
9789
TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx);
9790
}
9791
9792
bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
9793
{
9794
ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
9795
if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
9796
return false;
9797
9798
//IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools
9799
int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset;
9800
if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
9801
return false;
9802
9803
// Reordered tabs must share the same section
9804
// (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too)
9805
ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order];
9806
if (tab2->Flags & ImGuiTabItemFlags_NoReorder)
9807
return false;
9808
if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_))
9809
return false;
9810
9811
ImGuiTabItem item_tmp = *tab1;
9812
ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2;
9813
ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1;
9814
const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset;
9815
memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem));
9816
*tab2 = item_tmp;
9817
9818
if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
9819
MarkIniSettingsDirty();
9820
return true;
9821
}
9822
9823
static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
9824
{
9825
ImGuiContext& g = *GImGui;
9826
ImGuiWindow* window = g.CurrentWindow;
9827
9828
const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f);
9829
const float scrolling_buttons_width = arrow_button_size.x * 2.0f;
9830
9831
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9832
//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));
9833
9834
int select_dir = 0;
9835
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9836
arrow_col.w *= 0.5f;
9837
9838
PushStyleColor(ImGuiCol_Text, arrow_col);
9839
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
9840
PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
9841
const float backup_repeat_delay = g.IO.KeyRepeatDelay;
9842
const float backup_repeat_rate = g.IO.KeyRepeatRate;
9843
g.IO.KeyRepeatDelay = 0.250f;
9844
g.IO.KeyRepeatRate = 0.200f;
9845
float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width);
9846
window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y);
9847
if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
9848
select_dir = -1;
9849
window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y);
9850
if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick))
9851
select_dir = +1;
9852
PopItemFlag();
9853
PopStyleColor(2);
9854
g.IO.KeyRepeatRate = backup_repeat_rate;
9855
g.IO.KeyRepeatDelay = backup_repeat_delay;
9856
9857
ImGuiTabItem* tab_to_scroll_to = NULL;
9858
if (select_dir != 0)
9859
if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId))
9860
{
9861
int selected_order = TabBarGetTabOrder(tab_bar, tab_item);
9862
int target_order = selected_order + select_dir;
9863
9864
// Skip tab item buttons until another tab item is found or end is reached
9865
while (tab_to_scroll_to == NULL)
9866
{
9867
// If we are at the end of the list, still scroll to make our tab visible
9868
tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order];
9869
9870
// Cross through buttons
9871
// (even if first/last item is a button, return it so we can update the scroll)
9872
if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button)
9873
{
9874
target_order += select_dir;
9875
selected_order += select_dir;
9876
tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL;
9877
}
9878
}
9879
}
9880
window->DC.CursorPos = backup_cursor_pos;
9881
tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f;
9882
9883
return tab_to_scroll_to;
9884
}
9885
9886
static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
9887
{
9888
ImGuiContext& g = *GImGui;
9889
ImGuiWindow* window = g.CurrentWindow;
9890
9891
// We use g.Style.FramePadding.y to match the square ArrowButton size
9892
const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y;
9893
const ImVec2 backup_cursor_pos = window->DC.CursorPos;
9894
window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y);
9895
tab_bar->BarRect.Min.x += tab_list_popup_button_width;
9896
9897
ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text];
9898
arrow_col.w *= 0.5f;
9899
PushStyleColor(ImGuiCol_Text, arrow_col);
9900
PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
9901
bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest);
9902
PopStyleColor(2);
9903
9904
ImGuiTabItem* tab_to_select = NULL;
9905
if (open)
9906
{
9907
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
9908
{
9909
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
9910
if (tab->Flags & ImGuiTabItemFlags_Button)
9911
continue;
9912
9913
const char* tab_name = TabBarGetTabName(tab_bar, tab);
9914
if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
9915
tab_to_select = tab;
9916
}
9917
EndCombo();
9918
}
9919
9920
window->DC.CursorPos = backup_cursor_pos;
9921
return tab_to_select;
9922
}
9923
9924
//-------------------------------------------------------------------------
9925
// [SECTION] Widgets: BeginTabItem, EndTabItem, etc.
9926
//-------------------------------------------------------------------------
9927
// - BeginTabItem()
9928
// - EndTabItem()
9929
// - TabItemButton()
9930
// - TabItemEx() [Internal]
9931
// - SetTabItemClosed()
9932
// - TabItemCalcSize() [Internal]
9933
// - TabItemBackground() [Internal]
9934
// - TabItemLabelAndCloseButton() [Internal]
9935
//-------------------------------------------------------------------------
9936
9937
bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags)
9938
{
9939
ImGuiContext& g = *GImGui;
9940
ImGuiWindow* window = g.CurrentWindow;
9941
if (window->SkipItems)
9942
return false;
9943
9944
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9945
if (tab_bar == NULL)
9946
{
9947
IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
9948
return false;
9949
}
9950
IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead!
9951
9952
bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL);
9953
if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
9954
{
9955
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9956
PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label)
9957
}
9958
return ret;
9959
}
9960
9961
void ImGui::EndTabItem()
9962
{
9963
ImGuiContext& g = *GImGui;
9964
ImGuiWindow* window = g.CurrentWindow;
9965
if (window->SkipItems)
9966
return;
9967
9968
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9969
if (tab_bar == NULL)
9970
{
9971
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
9972
return;
9973
}
9974
IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
9975
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
9976
if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
9977
PopID();
9978
}
9979
9980
bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags)
9981
{
9982
ImGuiContext& g = *GImGui;
9983
ImGuiWindow* window = g.CurrentWindow;
9984
if (window->SkipItems)
9985
return false;
9986
9987
ImGuiTabBar* tab_bar = g.CurrentTabBar;
9988
if (tab_bar == NULL)
9989
{
9990
IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!");
9991
return false;
9992
}
9993
return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL);
9994
}
9995
9996
bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window)
9997
{
9998
// Layout whole tab bar if not already done
9999
ImGuiContext& g = *GImGui;
10000
if (tab_bar->WantLayout)
10001
{
10002
ImGuiNextItemData backup_next_item_data = g.NextItemData;
10003
TabBarLayout(tab_bar);
10004
g.NextItemData = backup_next_item_data;
10005
}
10006
ImGuiWindow* window = g.CurrentWindow;
10007
if (window->SkipItems)
10008
return false;
10009
10010
const ImGuiStyle& style = g.Style;
10011
const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window);
10012
10013
// If the user called us with *p_open == false, we early out and don't render.
10014
// 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.
10015
IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags);
10016
if (p_open && !*p_open)
10017
{
10018
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10019
return false;
10020
}
10021
10022
IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button));
10023
IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing
10024
10025
// Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
10026
if (flags & ImGuiTabItemFlags_NoCloseButton)
10027
p_open = NULL;
10028
else if (p_open == NULL)
10029
flags |= ImGuiTabItemFlags_NoCloseButton;
10030
10031
// Acquire tab data
10032
ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id);
10033
bool tab_is_new = false;
10034
if (tab == NULL)
10035
{
10036
tab_bar->Tabs.push_back(ImGuiTabItem());
10037
tab = &tab_bar->Tabs.back();
10038
tab->ID = id;
10039
tab_bar->TabsAddedNew = tab_is_new = true;
10040
}
10041
tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab);
10042
10043
// Calculate tab contents size
10044
ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument));
10045
tab->RequestedWidth = -1.0f;
10046
if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth)
10047
size.x = tab->RequestedWidth = g.NextItemData.Width;
10048
if (tab_is_new)
10049
tab->Width = ImMax(1.0f, size.x);
10050
tab->ContentWidth = size.x;
10051
tab->BeginOrder = tab_bar->TabsActiveCount++;
10052
10053
const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
10054
const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
10055
const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
10056
const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument);
10057
const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
10058
tab->LastFrameVisible = g.FrameCount;
10059
tab->Flags = flags;
10060
tab->Window = docked_window;
10061
10062
// Append name _WITH_ the zero-terminator
10063
// (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar)
10064
if (docked_window != NULL)
10065
{
10066
IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode);
10067
tab->NameOffset = -1;
10068
}
10069
else
10070
{
10071
tab->NameOffset = (ImS32)tab_bar->TabsNames.size();
10072
tab_bar->TabsNames.append(label, label + strlen(label) + 1);
10073
}
10074
10075
// Update selected tab
10076
if (!is_tab_button)
10077
{
10078
if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
10079
if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
10080
TabBarQueueFocus(tab_bar, tab); // New tabs gets activated
10081
if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar
10082
TabBarQueueFocus(tab_bar, tab);
10083
}
10084
10085
// Lock visibility
10086
// (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!)
10087
bool tab_contents_visible = (tab_bar->VisibleTabId == id);
10088
if (tab_contents_visible)
10089
tab_bar->VisibleTabWasSubmitted = true;
10090
10091
// On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches
10092
if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL)
10093
if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs))
10094
tab_contents_visible = true;
10095
10096
// Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted
10097
// and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'.
10098
if (tab_appearing && (!tab_bar_appearing || tab_is_new))
10099
{
10100
ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav);
10101
if (is_tab_button)
10102
return false;
10103
return tab_contents_visible;
10104
}
10105
10106
if (tab_bar->SelectedTabId == id)
10107
tab->LastFrameSelected = g.FrameCount;
10108
10109
// Backup current layout position
10110
const ImVec2 backup_main_cursor_pos = window->DC.CursorPos;
10111
10112
// Layout
10113
const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0;
10114
size.x = tab->Width;
10115
if (is_central_section)
10116
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
10117
else
10118
window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f);
10119
ImVec2 pos = window->DC.CursorPos;
10120
ImRect bb(pos, pos + size);
10121
10122
// 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)
10123
const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX);
10124
if (want_clip_rect)
10125
PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true);
10126
10127
ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
10128
ItemSize(bb.GetSize(), style.FramePadding.y);
10129
window->DC.CursorMaxPos = backup_cursor_max_pos;
10130
10131
if (!ItemAdd(bb, id))
10132
{
10133
if (want_clip_rect)
10134
PopClipRect();
10135
window->DC.CursorPos = backup_main_cursor_pos;
10136
return tab_contents_visible;
10137
}
10138
10139
// Click to Select a tab
10140
ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap);
10141
if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this
10142
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
10143
bool hovered, held;
10144
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
10145
if (pressed && !is_tab_button)
10146
TabBarQueueFocus(tab_bar, tab);
10147
10148
// Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow()
10149
// will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id.
10150
if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated)
10151
g.ActiveIdWindow = docked_window;
10152
10153
// Drag and drop a single floating window node moves it
10154
ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL;
10155
const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1);
10156
if (held && single_floating_window_node && IsMouseDragging(0, 0.0f))
10157
{
10158
// Move
10159
StartMouseMovingWindow(docked_window);
10160
}
10161
else if (held && !tab_appearing && IsMouseDragging(0))
10162
{
10163
// Drag and drop: re-order tabs
10164
int drag_dir = 0;
10165
float drag_distance_from_edge_x = 0.0f;
10166
if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL)))
10167
{
10168
// While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x
10169
if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x)
10170
{
10171
drag_dir = -1;
10172
drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x;
10173
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10174
}
10175
else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x)
10176
{
10177
drag_dir = +1;
10178
drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x;
10179
TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos);
10180
}
10181
}
10182
10183
// Extract a Dockable window out of it's tab bar
10184
const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking);
10185
if (can_undock)
10186
{
10187
// We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar
10188
bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id);
10189
if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift)
10190
{
10191
float threshold_base = g.FontSize;
10192
float threshold_x = (threshold_base * 2.2f);
10193
float threshold_y = (threshold_base * 1.5f) + ImClamp((ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, 0.0f, threshold_base * 4.0f);
10194
//GetForegroundDrawList()->AddRect(ImVec2(bb.Min.x - threshold_x, bb.Min.y - threshold_y), ImVec2(bb.Max.x + threshold_x, bb.Max.y + threshold_y), IM_COL32_WHITE); // [DEBUG]
10195
10196
float distance_from_edge_y = ImMax(bb.Min.y - g.IO.MousePos.y, g.IO.MousePos.y - bb.Max.y);
10197
if (distance_from_edge_y >= threshold_y)
10198
undocking_tab = true;
10199
if (drag_distance_from_edge_x > threshold_x)
10200
if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1))
10201
undocking_tab = true;
10202
}
10203
10204
if (undocking_tab)
10205
{
10206
// Undock
10207
// FIXME: refactor to share more code with e.g. StartMouseMovingWindow
10208
DockContextQueueUndockWindow(&g, docked_window);
10209
g.MovingWindow = docked_window;
10210
SetActiveID(g.MovingWindow->MoveId, g.MovingWindow);
10211
g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min;
10212
g.ActiveIdNoClearOnFocusLoss = true;
10213
SetActiveIdUsingAllKeyboardKeys();
10214
}
10215
}
10216
}
10217
10218
#if 0
10219
if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth)
10220
{
10221
// Enlarge tab display when hovering
10222
bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)));
10223
display_draw_list = GetForegroundDrawList(window);
10224
TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive));
10225
}
10226
#endif
10227
10228
// Render tab shape
10229
ImDrawList* display_draw_list = window->DrawList;
10230
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));
10231
TabItemBackground(display_draw_list, bb, flags, tab_col);
10232
if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f)
10233
{
10234
float x_offset = IM_TRUNC(0.4f * style.TabRounding);
10235
if (x_offset < 2.0f * g.CurrentDpiScale)
10236
x_offset = 0.0f;
10237
float y_offset = 1.0f * g.CurrentDpiScale;
10238
display_draw_list->AddLine(bb.GetTL() + ImVec2(x_offset, y_offset), bb.GetTR() + ImVec2(-x_offset, y_offset), GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline), style.TabBarOverlineSize);
10239
}
10240
RenderNavCursor(bb, id);
10241
10242
// Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget.
10243
const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
10244
if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button)
10245
TabBarQueueFocus(tab_bar, tab);
10246
10247
if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)
10248
flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton;
10249
10250
// Render tab label, process close button
10251
const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, docked_window ? docked_window->ID : id) : 0;
10252
bool just_closed;
10253
bool text_clipped;
10254
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);
10255
if (just_closed && p_open != NULL)
10256
{
10257
*p_open = false;
10258
TabBarCloseTab(tab_bar, tab);
10259
}
10260
10261
// Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent)
10262
// That state is copied to window->DockTabItemStatusFlags by our caller.
10263
if (docked_window && (hovered || g.HoveredId == close_button_id))
10264
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow;
10265
10266
// Restore main window position so user can draw there
10267
if (want_clip_rect)
10268
PopClipRect();
10269
window->DC.CursorPos = backup_main_cursor_pos;
10270
10271
// Tooltip
10272
// (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok)
10273
// (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores)
10274
// FIXME: This is a mess.
10275
// FIXME: We may want disabled tab to still display the tooltip?
10276
if (text_clipped && g.HoveredId == id && !held)
10277
if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
10278
SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label);
10279
10280
IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected
10281
if (is_tab_button)
10282
return pressed;
10283
return tab_contents_visible;
10284
}
10285
10286
// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed.
10287
// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar().
10288
// Tabs closed by the close button will automatically be flagged to avoid this issue.
10289
void ImGui::SetTabItemClosed(const char* label)
10290
{
10291
ImGuiContext& g = *GImGui;
10292
bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode);
10293
if (is_within_manual_tab_bar)
10294
{
10295
ImGuiTabBar* tab_bar = g.CurrentTabBar;
10296
ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL);
10297
if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id))
10298
tab->WantClose = true; // Will be processed by next call to TabBarLayout()
10299
}
10300
else if (ImGuiWindow* window = FindWindowByName(label))
10301
{
10302
if (window->DockIsActive)
10303
if (ImGuiDockNode* node = window->DockNode)
10304
{
10305
ImGuiID tab_id = TabBarCalcTabID(node->TabBar, label, window);
10306
TabBarRemoveTab(node->TabBar, tab_id);
10307
window->DockTabWantClose = true;
10308
}
10309
}
10310
}
10311
10312
ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker)
10313
{
10314
ImGuiContext& g = *GImGui;
10315
ImVec2 label_size = CalcTextSize(label, NULL, true);
10316
ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f);
10317
if (has_close_button_or_unsaved_marker)
10318
size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle.
10319
else
10320
size.x += g.Style.FramePadding.x + 1.0f;
10321
return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y);
10322
}
10323
10324
ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window)
10325
{
10326
return TabItemCalcSize(window->Name, window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument));
10327
}
10328
10329
void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col)
10330
{
10331
// 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.
10332
ImGuiContext& g = *GImGui;
10333
const float width = bb.GetWidth();
10334
IM_UNUSED(flags);
10335
IM_ASSERT(width > 0.0f);
10336
const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f));
10337
const float y1 = bb.Min.y + 1.0f;
10338
const float y2 = bb.Max.y - g.Style.TabBarBorderSize;
10339
draw_list->PathLineTo(ImVec2(bb.Min.x, y2));
10340
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9);
10341
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12);
10342
draw_list->PathLineTo(ImVec2(bb.Max.x, y2));
10343
draw_list->PathFillConvex(col);
10344
if (g.Style.TabBorderSize > 0.0f)
10345
{
10346
draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2));
10347
draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9);
10348
draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12);
10349
draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2));
10350
draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize);
10351
}
10352
}
10353
10354
// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic
10355
// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter.
10356
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)
10357
{
10358
ImGuiContext& g = *GImGui;
10359
ImVec2 label_size = CalcTextSize(label, NULL, true);
10360
10361
if (out_just_closed)
10362
*out_just_closed = false;
10363
if (out_text_clipped)
10364
*out_text_clipped = false;
10365
10366
if (bb.GetWidth() <= 1.0f)
10367
return;
10368
10369
// In Style V2 we'll have full override of all colors per state (e.g. focused, selected)
10370
// But right now if you want to alter text color of tabs this is what you need to do.
10371
#if 0
10372
const float backup_alpha = g.Style.Alpha;
10373
if (!is_contents_visible)
10374
g.Style.Alpha *= 0.7f;
10375
#endif
10376
10377
// Render text label (with clipping + alpha gradient) + unsaved marker
10378
ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y);
10379
ImRect text_ellipsis_clip_bb = text_pixel_clip_bb;
10380
10381
// Return clipped state ignoring the close button
10382
if (out_text_clipped)
10383
{
10384
*out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x;
10385
//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));
10386
}
10387
10388
const float button_sz = g.FontSize;
10389
const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y);
10390
10391
// Close Button & Unsaved Marker
10392
// We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()
10393
// 'hovered' will be true when hovering the Tab but NOT when hovering the close button
10394
// 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button
10395
// 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false
10396
bool close_button_pressed = false;
10397
bool close_button_visible = false;
10398
if (close_button_id != 0)
10399
if (is_contents_visible || bb.GetWidth() >= ImMax(button_sz, g.Style.TabMinWidthForCloseButton))
10400
if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id)
10401
close_button_visible = true;
10402
bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x);
10403
10404
if (close_button_visible)
10405
{
10406
ImGuiLastItemData last_item_backup = g.LastItemData;
10407
if (CloseButton(close_button_id, button_pos))
10408
close_button_pressed = true;
10409
g.LastItemData = last_item_backup;
10410
10411
// Close with middle mouse button
10412
if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2))
10413
close_button_pressed = true;
10414
}
10415
else if (unsaved_marker_visible)
10416
{
10417
const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz));
10418
RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text));
10419
}
10420
10421
// This is all rather complicated
10422
// (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position)
10423
// 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..
10424
float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f;
10425
if (close_button_visible || unsaved_marker_visible)
10426
{
10427
text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f);
10428
text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f;
10429
ellipsis_max_x = text_pixel_clip_bb.Max.x;
10430
}
10431
LogSetNextTextDecoration("/", "\\");
10432
RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size);
10433
10434
#if 0
10435
if (!is_contents_visible)
10436
g.Style.Alpha = backup_alpha;
10437
#endif
10438
10439
if (out_just_closed)
10440
*out_just_closed = close_button_pressed;
10441
}
10442
10443
10444
#endif // #ifndef IMGUI_DISABLE
10445
10446