| // dear imgui, v1.64 WIP |
| // (widgets code) |
| |
| #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) |
| #define _CRT_SECURE_NO_WARNINGS |
| #endif |
| |
| #include "imgui.h" |
| #ifndef IMGUI_DEFINE_MATH_OPERATORS |
| #define IMGUI_DEFINE_MATH_OPERATORS |
| #endif |
| #include "imgui_internal.h" |
| |
| #include <ctype.h> // toupper, isprint |
| |
| // Visual Studio warnings |
| #ifdef _MSC_VER |
| #pragma warning (disable: 4127) // condition expression is constant |
| #pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen |
| #endif |
| |
| // Clang/GCC warnings with -Weverything |
| #ifdef __clang__ |
| #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. |
| #pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // |
| #elif defined(__GNUC__) |
| #pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked |
| #if __GNUC__ >= 8 |
| #pragma GCC diagnostic ignored "-Wclass-memaccess" // warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead |
| #endif |
| #endif |
| |
| //------------------------------------------------------------------------- |
| // Forward Declarations |
| //------------------------------------------------------------------------- |
| |
| // For InputTextEx() |
| static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); |
| static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); |
| static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); |
| |
| //------------------------------------------------------------------------- |
| // SHARED UTILITIES |
| //------------------------------------------------------------------------- |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Text |
| // - TextUnformatted() |
| // - Text() |
| // - TextV() |
| // - TextColored() |
| // - TextColoredV() |
| // - TextDisabled() |
| // - TextDisabledV() |
| // - TextWrapped() |
| // - TextWrappedV() |
| // - LabelText() |
| // - LabelTextV() |
| // - BulletText() |
| // - BulletTextV() |
| //------------------------------------------------------------------------- |
| |
| void ImGui::TextUnformatted(const char* text, const char* text_end) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| IM_ASSERT(text != NULL); |
| const char* text_begin = text; |
| if (text_end == NULL) |
| text_end = text + strlen(text); // FIXME-OPT |
| |
| const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrentLineTextBaseOffset); |
| const float wrap_pos_x = window->DC.TextWrapPos; |
| const bool wrap_enabled = wrap_pos_x >= 0.0f; |
| if (text_end - text > 2000 && !wrap_enabled) |
| { |
| // Long text! |
| // Perform manual coarse clipping to optimize for long multi-line text |
| // From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. |
| // 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. |
| const char* line = text; |
| const float line_height = GetTextLineHeight(); |
| const ImRect clip_rect = window->ClipRect; |
| ImVec2 text_size(0,0); |
| |
| if (text_pos.y <= clip_rect.Max.y) |
| { |
| ImVec2 pos = text_pos; |
| |
| // Lines to skip (can't skip when logging text) |
| if (!g.LogEnabled) |
| { |
| int lines_skippable = (int)((clip_rect.Min.y - text_pos.y) / line_height); |
| if (lines_skippable > 0) |
| { |
| int lines_skipped = 0; |
| while (line < text_end && lines_skipped < lines_skippable) |
| { |
| const char* line_end = strchr(line, '\n'); |
| if (!line_end) |
| line_end = text_end; |
| line = line_end + 1; |
| lines_skipped++; |
| } |
| pos.y += lines_skipped * line_height; |
| } |
| } |
| |
| // Lines to render |
| if (line < text_end) |
| { |
| ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); |
| while (line < text_end) |
| { |
| const char* line_end = strchr(line, '\n'); |
| if (IsClippedEx(line_rect, 0, false)) |
| break; |
| |
| const ImVec2 line_size = CalcTextSize(line, line_end, false); |
| text_size.x = ImMax(text_size.x, line_size.x); |
| RenderText(pos, line, line_end, false); |
| if (!line_end) |
| line_end = text_end; |
| line = line_end + 1; |
| line_rect.Min.y += line_height; |
| line_rect.Max.y += line_height; |
| pos.y += line_height; |
| } |
| |
| // Count remaining lines |
| int lines_skipped = 0; |
| while (line < text_end) |
| { |
| const char* line_end = strchr(line, '\n'); |
| if (!line_end) |
| line_end = text_end; |
| line = line_end + 1; |
| lines_skipped++; |
| } |
| pos.y += lines_skipped * line_height; |
| } |
| |
| text_size.y += (pos - text_pos).y; |
| } |
| |
| ImRect bb(text_pos, text_pos + text_size); |
| ItemSize(bb); |
| ItemAdd(bb, 0); |
| } |
| else |
| { |
| const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; |
| const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); |
| |
| // Account of baseline offset |
| ImRect bb(text_pos, text_pos + text_size); |
| ItemSize(text_size); |
| if (!ItemAdd(bb, 0)) |
| return; |
| |
| // Render (we don't hide text after ## in this end-user function) |
| RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); |
| } |
| } |
| |
| void ImGui::Text(const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| TextV(fmt, args); |
| va_end(args); |
| } |
| |
| void ImGui::TextV(const char* fmt, va_list args) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); |
| TextUnformatted(g.TempBuffer, text_end); |
| } |
| |
| void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| TextColoredV(col, fmt, args); |
| va_end(args); |
| } |
| |
| void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) |
| { |
| PushStyleColor(ImGuiCol_Text, col); |
| TextV(fmt, args); |
| PopStyleColor(); |
| } |
| |
| void ImGui::TextDisabled(const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| TextDisabledV(fmt, args); |
| va_end(args); |
| } |
| |
| void ImGui::TextDisabledV(const char* fmt, va_list args) |
| { |
| PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); |
| TextV(fmt, args); |
| PopStyleColor(); |
| } |
| |
| void ImGui::TextWrapped(const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| TextWrappedV(fmt, args); |
| va_end(args); |
| } |
| |
| void ImGui::TextWrappedV(const char* fmt, va_list args) |
| { |
| bool need_wrap = (GImGui->CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position is one ia already set |
| if (need_wrap) PushTextWrapPos(0.0f); |
| TextV(fmt, args); |
| if (need_wrap) PopTextWrapPos(); |
| } |
| |
| void ImGui::LabelText(const char* label, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| LabelTextV(label, fmt, args); |
| va_end(args); |
| } |
| |
| // Add a label+text combo aligned to other label+value widgets |
| void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const float w = CalcItemWidth(); |
| |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2)); |
| const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y*2) + label_size); |
| ItemSize(total_bb, style.FramePadding.y); |
| if (!ItemAdd(total_bb, 0)) |
| return; |
| |
| // Render |
| const char* value_text_begin = &g.TempBuffer[0]; |
| const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); |
| RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f,0.5f)); |
| if (label_size.x > 0.0f) |
| RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); |
| } |
| |
| void ImGui::BulletText(const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| BulletTextV(fmt, args); |
| va_end(args); |
| } |
| |
| // Text with a little bullet aligned to the typical tree node. |
| void ImGui::BulletTextV(const char* fmt, va_list args) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| |
| const char* text_begin = g.TempBuffer; |
| const char* text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); |
| const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); |
| const float text_base_offset_y = ImMax(0.0f, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it |
| const float line_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x*2) : 0.0f), ImMax(line_height, label_size.y))); // Empty text doesn't add padding |
| ItemSize(bb); |
| if (!ItemAdd(bb, 0)) |
| return; |
| |
| // Render |
| RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f)); |
| RenderText(bb.Min+ImVec2(g.FontSize + style.FramePadding.x*2, text_base_offset_y), text_begin, text_end, false); |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Main |
| // - ButtonBehavior() [Internal] |
| // - Button() |
| // - SmallButton() |
| // - InvisibleButton() |
| // - ArrowButton() |
| // - CloseButton() [Internal] |
| // - CollapseButton() [Internal] |
| // - Image() |
| // - ImageButton() |
| // - Checkbox() |
| // - CheckboxFlags() |
| // - RadioButton() |
| // - ProgressBar() |
| // - Bullet() |
| //------------------------------------------------------------------------- |
| |
| bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) |
| { |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = GetCurrentWindow(); |
| |
| if (flags & ImGuiButtonFlags_Disabled) |
| { |
| if (out_hovered) *out_hovered = false; |
| if (out_held) *out_held = false; |
| if (g.ActiveId == id) ClearActiveID(); |
| return false; |
| } |
| |
| // Default behavior requires click+release on same spot |
| if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0) |
| flags |= ImGuiButtonFlags_PressedOnClickRelease; |
| |
| ImGuiWindow* backup_hovered_window = g.HoveredWindow; |
| if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) |
| g.HoveredWindow = window; |
| |
| bool pressed = false; |
| bool hovered = ItemHoverable(bb, id); |
| |
| // Drag source doesn't report as hovered |
| if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) |
| hovered = false; |
| |
| // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button |
| if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) |
| if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) |
| { |
| hovered = true; |
| SetHoveredID(id); |
| if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy |
| { |
| pressed = true; |
| FocusWindow(window); |
| } |
| } |
| |
| if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) |
| g.HoveredWindow = backup_hovered_window; |
| |
| // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. |
| if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) |
| hovered = false; |
| |
| // Mouse |
| if (hovered) |
| { |
| if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) |
| { |
| // | CLICKING | HOLDING with ImGuiButtonFlags_Repeat |
| // PressedOnClickRelease | <on release>* | <on repeat> <on repeat> .. (NOT on release) <-- MOST COMMON! (*) only if both click/release were over bounds |
| // PressedOnClick | <on click> | <on click> <on repeat> <on repeat> .. |
| // PressedOnRelease | <on release> | <on repeat> <on repeat> .. (NOT on release) |
| // PressedOnDoubleClick | <on dclick> | <on dclick> <on repeat> <on repeat> .. |
| // FIXME-NAV: We don't honor those different behaviors. |
| if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0]) |
| { |
| SetActiveID(id, window); |
| if (!(flags & ImGuiButtonFlags_NoNavFocus)) |
| SetFocusID(id, window); |
| FocusWindow(window); |
| } |
| if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0])) |
| { |
| pressed = true; |
| if (flags & ImGuiButtonFlags_NoHoldingActiveID) |
| ClearActiveID(); |
| else |
| SetActiveID(id, window); // Hold on ID |
| FocusWindow(window); |
| } |
| if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0]) |
| { |
| if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps <on release> |
| pressed = true; |
| ClearActiveID(); |
| } |
| |
| // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). |
| // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. |
| if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true)) |
| pressed = true; |
| } |
| |
| if (pressed) |
| g.NavDisableHighlight = true; |
| } |
| |
| // Gamepad/Keyboard navigation |
| // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. |
| if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) |
| hovered = true; |
| |
| if (g.NavActivateDownId == id) |
| { |
| bool nav_activated_by_code = (g.NavActivateId == id); |
| bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); |
| if (nav_activated_by_code || nav_activated_by_inputs) |
| pressed = true; |
| if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id) |
| { |
| // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. |
| g.NavActivateId = id; // This is so SetActiveId assign a Nav source |
| SetActiveID(id, window); |
| if (!(flags & ImGuiButtonFlags_NoNavFocus)) |
| SetFocusID(id, window); |
| g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); |
| } |
| } |
| |
| bool held = false; |
| if (g.ActiveId == id) |
| { |
| if (g.ActiveIdSource == ImGuiInputSource_Mouse) |
| { |
| if (g.ActiveIdIsJustActivated) |
| g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; |
| if (g.IO.MouseDown[0]) |
| { |
| held = true; |
| } |
| else |
| { |
| if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease)) |
| if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps <on release> |
| if (!g.DragDropActive) |
| pressed = true; |
| ClearActiveID(); |
| } |
| if (!(flags & ImGuiButtonFlags_NoNavFocus)) |
| g.NavDisableHighlight = true; |
| } |
| else if (g.ActiveIdSource == ImGuiInputSource_Nav) |
| { |
| if (g.NavActivateDownId != id) |
| ClearActiveID(); |
| } |
| } |
| |
| if (out_hovered) *out_hovered = hovered; |
| if (out_held) *out_held = held; |
| |
| return pressed; |
| } |
| |
| bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const ImGuiID id = window->GetID(label); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| ImVec2 pos = window->DC.CursorPos; |
| if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrentLineTextBaseOffset) // 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) |
| pos.y += window->DC.CurrentLineTextBaseOffset - style.FramePadding.y; |
| ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); |
| |
| const ImRect bb(pos, pos + size); |
| ItemSize(bb, style.FramePadding.y); |
| if (!ItemAdd(bb, id)) |
| return false; |
| |
| if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) |
| flags |= ImGuiButtonFlags_Repeat; |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); |
| if (pressed) |
| MarkItemEdited(id); |
| |
| // Render |
| const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); |
| RenderNavHighlight(bb, id); |
| RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); |
| RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); |
| |
| // Automatically close popups |
| //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) |
| // CloseCurrentPopup(); |
| |
| return pressed; |
| } |
| |
| bool ImGui::Button(const char* label, const ImVec2& size_arg) |
| { |
| return ButtonEx(label, size_arg, 0); |
| } |
| |
| // Small buttons fits within text without additional vertical spacing. |
| bool ImGui::SmallButton(const char* label) |
| { |
| ImGuiContext& g = *GImGui; |
| float backup_padding_y = g.Style.FramePadding.y; |
| g.Style.FramePadding.y = 0.0f; |
| bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine); |
| g.Style.FramePadding.y = backup_padding_y; |
| return pressed; |
| } |
| |
| // Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. |
| // 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) |
| bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size. |
| IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f); |
| |
| const ImGuiID id = window->GetID(str_id); |
| ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); |
| ItemSize(bb); |
| if (!ItemAdd(bb, id)) |
| return false; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held); |
| |
| return pressed; |
| } |
| |
| bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiID id = window->GetID(str_id); |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); |
| const float default_size = GetFrameHeight(); |
| ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); |
| if (!ItemAdd(bb, id)) |
| return false; |
| |
| if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) |
| flags |= ImGuiButtonFlags_Repeat; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); |
| |
| // Render |
| const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); |
| RenderNavHighlight(bb, id); |
| RenderFrame(bb.Min, bb.Max, col, true, g.Style.FrameRounding); |
| RenderArrow(bb.Min + ImVec2(ImMax(0.0f, size.x - g.FontSize - g.Style.FramePadding.x), ImMax(0.0f, size.y - g.FontSize - g.Style.FramePadding.y)), dir); |
| |
| return pressed; |
| } |
| |
| bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir) |
| { |
| float sz = GetFrameHeight(); |
| return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), 0); |
| } |
| |
| // Button to close a window |
| bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos, float radius) |
| { |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = g.CurrentWindow; |
| |
| // We intentionally allow interaction when clipped so that a mechanical Alt,Right,Validate sequence close a window. |
| // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). |
| const ImRect bb(pos - ImVec2(radius,radius), pos + ImVec2(radius,radius)); |
| bool is_clipped = !ItemAdd(bb, id); |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held); |
| if (is_clipped) |
| return pressed; |
| |
| // Render |
| ImVec2 center = bb.GetCenter(); |
| if (hovered) |
| window->DrawList->AddCircleFilled(center, ImMax(2.0f, radius), GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 9); |
| |
| float cross_extent = (radius * 0.7071f) - 1.0f; |
| ImU32 cross_col = GetColorU32(ImGuiCol_Text); |
| center -= ImVec2(0.5f, 0.5f); |
| window->DrawList->AddLine(center + ImVec2(+cross_extent,+cross_extent), center + ImVec2(-cross_extent,-cross_extent), cross_col, 1.0f); |
| window->DrawList->AddLine(center + ImVec2(+cross_extent,-cross_extent), center + ImVec2(-cross_extent,+cross_extent), cross_col, 1.0f); |
| |
| return pressed; |
| } |
| |
| bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) |
| { |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = g.CurrentWindow; |
| |
| ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); |
| ItemAdd(bb, id); |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); |
| |
| ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); |
| if (hovered || held) |
| window->DrawList->AddCircleFilled(bb.GetCenter() + ImVec2(0.0f, -0.5f), g.FontSize * 0.5f + 1.0f, col, 9); |
| RenderArrow(bb.Min + g.Style.FramePadding, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); |
| |
| // Switch to moving the window after mouse is moved beyond the initial drag threshold |
| if (IsItemActive() && IsMouseDragging()) |
| StartMouseMovingWindow(window); |
| |
| return pressed; |
| } |
| |
| void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); |
| if (border_col.w > 0.0f) |
| bb.Max += ImVec2(2, 2); |
| ItemSize(bb); |
| if (!ItemAdd(bb, 0)) |
| return; |
| |
| if (border_col.w > 0.0f) |
| { |
| window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); |
| window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col)); |
| } |
| else |
| { |
| window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); |
| } |
| } |
| |
| // frame_padding < 0: uses FramePadding from style (default) |
| // frame_padding = 0: no framing |
| // frame_padding > 0: set framing size |
| // The color used are the button colors. |
| 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) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| |
| // Default to using texture ID as ID. User can still push string/integer prefixes. |
| // We could hash the size/uv to create a unique ID but that would prevent the user from animating UV. |
| PushID((void*)user_texture_id); |
| const ImGuiID id = window->GetID("#image"); |
| PopID(); |
| |
| const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : style.FramePadding; |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2); |
| const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size); |
| ItemSize(bb); |
| if (!ItemAdd(bb, id)) |
| return false; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held); |
| |
| // Render |
| const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); |
| RenderNavHighlight(bb, id); |
| RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, style.FrameRounding)); |
| if (bg_col.w > 0.0f) |
| window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col)); |
| window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col)); |
| |
| return pressed; |
| } |
| |
| bool ImGui::Checkbox(const char* label, bool* v) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const ImGuiID id = window->GetID(label); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2, label_size.y + style.FramePadding.y*2)); // We want a square shape to we use Y twice |
| ItemSize(check_bb, style.FramePadding.y); |
| |
| ImRect total_bb = check_bb; |
| if (label_size.x > 0) |
| SameLine(0, style.ItemInnerSpacing.x); |
| const ImRect text_bb(window->DC.CursorPos + ImVec2(0,style.FramePadding.y), window->DC.CursorPos + ImVec2(0,style.FramePadding.y) + label_size); |
| if (label_size.x > 0) |
| { |
| ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); |
| total_bb = ImRect(ImMin(check_bb.Min, text_bb.Min), ImMax(check_bb.Max, text_bb.Max)); |
| } |
| |
| if (!ItemAdd(total_bb, id)) |
| return false; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); |
| if (pressed) |
| { |
| *v = !(*v); |
| MarkItemEdited(id); |
| } |
| |
| RenderNavHighlight(total_bb, id); |
| RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); |
| if (*v) |
| { |
| const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); |
| const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f)); |
| RenderCheckMark(check_bb.Min + ImVec2(pad,pad), GetColorU32(ImGuiCol_CheckMark), check_bb.GetWidth() - pad*2.0f); |
| } |
| |
| if (g.LogEnabled) |
| LogRenderedText(&text_bb.Min, *v ? "[x]" : "[ ]"); |
| if (label_size.x > 0.0f) |
| RenderText(text_bb.Min, label); |
| |
| return pressed; |
| } |
| |
| bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) |
| { |
| bool v = ((*flags & flags_value) == flags_value); |
| bool pressed = Checkbox(label, &v); |
| if (pressed) |
| { |
| if (v) |
| *flags |= flags_value; |
| else |
| *flags &= ~flags_value; |
| } |
| |
| return pressed; |
| } |
| |
| bool ImGui::RadioButton(const char* label, bool active) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const ImGuiID id = window->GetID(label); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| const ImRect check_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(label_size.y + style.FramePadding.y*2-1, label_size.y + style.FramePadding.y*2-1)); |
| ItemSize(check_bb, style.FramePadding.y); |
| |
| ImRect total_bb = check_bb; |
| if (label_size.x > 0) |
| SameLine(0, style.ItemInnerSpacing.x); |
| const ImRect text_bb(window->DC.CursorPos + ImVec2(0, style.FramePadding.y), window->DC.CursorPos + ImVec2(0, style.FramePadding.y) + label_size); |
| if (label_size.x > 0) |
| { |
| ItemSize(ImVec2(text_bb.GetWidth(), check_bb.GetHeight()), style.FramePadding.y); |
| total_bb.Add(text_bb); |
| } |
| |
| if (!ItemAdd(total_bb, id)) |
| return false; |
| |
| ImVec2 center = check_bb.GetCenter(); |
| center.x = (float)(int)center.x + 0.5f; |
| center.y = (float)(int)center.y + 0.5f; |
| const float radius = check_bb.GetHeight() * 0.5f; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); |
| if (pressed) |
| MarkItemEdited(id); |
| |
| RenderNavHighlight(total_bb, id); |
| window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 16); |
| if (active) |
| { |
| const float check_sz = ImMin(check_bb.GetWidth(), check_bb.GetHeight()); |
| const float pad = ImMax(1.0f, (float)(int)(check_sz / 6.0f)); |
| window->DrawList->AddCircleFilled(center, radius-pad, GetColorU32(ImGuiCol_CheckMark), 16); |
| } |
| |
| if (style.FrameBorderSize > 0.0f) |
| { |
| window->DrawList->AddCircle(center+ImVec2(1,1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize); |
| window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize); |
| } |
| |
| if (g.LogEnabled) |
| LogRenderedText(&text_bb.Min, active ? "(x)" : "( )"); |
| if (label_size.x > 0.0f) |
| RenderText(text_bb.Min, label); |
| |
| return pressed; |
| } |
| |
| bool ImGui::RadioButton(const char* label, int* v, int v_button) |
| { |
| const bool pressed = RadioButton(label, *v == v_button); |
| if (pressed) |
| *v = v_button; |
| return pressed; |
| } |
| |
| // size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size |
| void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| |
| ImVec2 pos = window->DC.CursorPos; |
| ImRect bb(pos, pos + CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y*2.0f)); |
| ItemSize(bb, style.FramePadding.y); |
| if (!ItemAdd(bb, 0)) |
| return; |
| |
| // Render |
| fraction = ImSaturate(fraction); |
| RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); |
| bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); |
| const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); |
| RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); |
| |
| // Default displaying the fraction as percentage string, but user can override it |
| char overlay_buf[32]; |
| if (!overlay) |
| { |
| ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction*100+0.01f); |
| overlay = overlay_buf; |
| } |
| |
| ImVec2 overlay_size = CalcTextSize(overlay, NULL); |
| if (overlay_size.x > 0.0f) |
| RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.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); |
| } |
| |
| void ImGui::Bullet() |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const float line_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); |
| ItemSize(bb); |
| if (!ItemAdd(bb, 0)) |
| { |
| SameLine(0, style.FramePadding.x*2); |
| return; |
| } |
| |
| // Render and stay on same line |
| RenderBullet(bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f)); |
| SameLine(0, style.FramePadding.x*2); |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Combo Box |
| // - BeginCombo() |
| // - EndCombo() |
| // - Combo() |
| //------------------------------------------------------------------------- |
| |
| static float CalcMaxPopupHeightFromItemCount(int items_count) |
| { |
| ImGuiContext& g = *GImGui; |
| if (items_count <= 0) |
| return FLT_MAX; |
| return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); |
| } |
| |
| bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) |
| { |
| // Always consume the SetNextWindowSizeConstraint() call in our early return paths |
| ImGuiContext& g = *GImGui; |
| ImGuiCond backup_next_window_size_constraint = g.NextWindowData.SizeConstraintCond; |
| g.NextWindowData.SizeConstraintCond = 0; |
| |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together |
| |
| const ImGuiStyle& style = g.Style; |
| const ImGuiID id = window->GetID(label); |
| |
| const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : CalcItemWidth(); |
| const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); |
| 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)); |
| ItemSize(total_bb, style.FramePadding.y); |
| if (!ItemAdd(total_bb, id, &frame_bb)) |
| return false; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held); |
| bool popup_open = IsPopupOpen(id); |
| |
| const ImRect value_bb(frame_bb.Min, frame_bb.Max - ImVec2(arrow_size, 0.0f)); |
| const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); |
| RenderNavHighlight(frame_bb, id); |
| if (!(flags & ImGuiComboFlags_NoPreview)) |
| window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Max.y), frame_col, style.FrameRounding, ImDrawCornerFlags_Left); |
| if (!(flags & ImGuiComboFlags_NoArrowButton)) |
| { |
| window->DrawList->AddRectFilled(ImVec2(frame_bb.Max.x - arrow_size, frame_bb.Min.y), frame_bb.Max, GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button), style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right); |
| RenderArrow(ImVec2(frame_bb.Max.x - arrow_size + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), ImGuiDir_Down); |
| } |
| RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding); |
| if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) |
| RenderTextClipped(frame_bb.Min + style.FramePadding, value_bb.Max, preview_value, NULL, NULL, ImVec2(0.0f,0.0f)); |
| if (label_size.x > 0) |
| RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); |
| |
| if ((pressed || g.NavActivateId == id) && !popup_open) |
| { |
| if (window->DC.NavLayerCurrent == 0) |
| window->NavLastIds[0] = id; |
| OpenPopupEx(id); |
| popup_open = true; |
| } |
| |
| if (!popup_open) |
| return false; |
| |
| if (backup_next_window_size_constraint) |
| { |
| g.NextWindowData.SizeConstraintCond = backup_next_window_size_constraint; |
| g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); |
| } |
| else |
| { |
| if ((flags & ImGuiComboFlags_HeightMask_) == 0) |
| flags |= ImGuiComboFlags_HeightRegular; |
| IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one |
| int popup_max_height_in_items = -1; |
| if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; |
| else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; |
| else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; |
| SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); |
| } |
| |
| char name[16]; |
| ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.CurrentPopupStack.Size); // Recycle windows based on depth |
| |
| // Peak into expected window size so we can position it |
| if (ImGuiWindow* popup_window = FindWindowByName(name)) |
| if (popup_window->WasActive) |
| { |
| ImVec2 size_expected = CalcWindowExpectedSize(popup_window); |
| if (flags & ImGuiComboFlags_PopupAlignLeft) |
| popup_window->AutoPosLastDirection = ImGuiDir_Left; |
| ImRect r_outer = GetWindowAllowedExtentRect(popup_window); |
| ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox); |
| SetNextWindowPos(pos); |
| } |
| |
| // Horizontally align ourselves with the framed text |
| ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; |
| PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y)); |
| bool ret = Begin(name, NULL, window_flags); |
| PopStyleVar(); |
| if (!ret) |
| { |
| EndPopup(); |
| IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above |
| return false; |
| } |
| return true; |
| } |
| |
| void ImGui::EndCombo() |
| { |
| EndPopup(); |
| } |
| |
| // Getter for the old Combo() API: const char*[] |
| static bool Items_ArrayGetter(void* data, int idx, const char** out_text) |
| { |
| const char* const* items = (const char* const*)data; |
| if (out_text) |
| *out_text = items[idx]; |
| return true; |
| } |
| |
| // Getter for the old Combo() API: "item1\0item2\0item3\0" |
| static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) |
| { |
| // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. |
| const char* items_separated_by_zeros = (const char*)data; |
| int items_count = 0; |
| const char* p = items_separated_by_zeros; |
| while (*p) |
| { |
| if (idx == items_count) |
| break; |
| p += strlen(p) + 1; |
| items_count++; |
| } |
| if (!*p) |
| return false; |
| if (out_text) |
| *out_text = p; |
| return true; |
| } |
| |
| // Old API, prefer using BeginCombo() nowadays if you can. |
| bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items) |
| { |
| ImGuiContext& g = *GImGui; |
| |
| // Call the getter to obtain the preview string which is a parameter to BeginCombo() |
| const char* preview_value = NULL; |
| if (*current_item >= 0 && *current_item < items_count) |
| items_getter(data, *current_item, &preview_value); |
| |
| // 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. |
| if (popup_max_height_in_items != -1 && !g.NextWindowData.SizeConstraintCond) |
| SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); |
| |
| if (!BeginCombo(label, preview_value, ImGuiComboFlags_None)) |
| return false; |
| |
| // Display items |
| // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) |
| bool value_changed = false; |
| for (int i = 0; i < items_count; i++) |
| { |
| PushID((void*)(intptr_t)i); |
| const bool item_selected = (i == *current_item); |
| const char* item_text; |
| if (!items_getter(data, i, &item_text)) |
| item_text = "*Unknown item*"; |
| if (Selectable(item_text, item_selected)) |
| { |
| value_changed = true; |
| *current_item = i; |
| } |
| if (item_selected) |
| SetItemDefaultFocus(); |
| PopID(); |
| } |
| |
| EndCombo(); |
| return value_changed; |
| } |
| |
| // Combo box helper allowing to pass an array of strings. |
| bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) |
| { |
| const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items); |
| return value_changed; |
| } |
| |
| // Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0" |
| bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items) |
| { |
| int items_count = 0; |
| const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open |
| while (*p) |
| { |
| p += strlen(p) + 1; |
| items_count++; |
| } |
| bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); |
| return value_changed; |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Data Type and Data Formatting Helpers [Internal] |
| // - PatchFormatStringFloatToInt() |
| // - DataTypeFormatString() |
| // - DataTypeApplyOp() |
| // - DataTypeApplyOpFromText() |
| // - GetMinimumStepAtDecimalPrecision |
| // - RoundScalarWithFormat<>() |
| //------------------------------------------------------------------------- |
| |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Drags |
| // - DragBehaviorT<>() [Internal] |
| // - DragBehavior() [Internal] |
| // - DragScalar() |
| // - DragScalarN() |
| // - DragFloat() |
| // - DragFloat2() |
| // - DragFloat3() |
| // - DragFloat4() |
| // - DragFloatRange2() |
| // - DragInt() |
| // - DragInt2() |
| // - DragInt3() |
| // - DragInt4() |
| // - DragIntRange2() |
| //------------------------------------------------------------------------- |
| |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Sliders |
| // - SliderBehaviorT<>() [Internal] |
| // - SliderBehavior() [Internal] |
| // - SliderScalar() |
| // - SliderScalarN() |
| // - SliderFloat() |
| // - SliderFloat2() |
| // - SliderFloat3() |
| // - SliderFloat4() |
| // - SliderAngle() |
| // - SliderInt() |
| // - SliderInt2() |
| // - SliderInt3() |
| // - SliderInt4() |
| // - VSliderScalar() |
| // - VSliderFloat() |
| // - VSliderInt() |
| //------------------------------------------------------------------------- |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Inputs (_excepted InputText_) |
| // - ImParseFormatFindStart() |
| // - ImParseFormatFindEnd() |
| // - ImParseFormatTrimDecorations() |
| // - ImParseFormatPrecision() |
| // - InputScalarAsWidgetReplacement() [Internal] |
| // - InputScalar() |
| // - InputScalarN() |
| // - InputFloat() |
| // - InputFloat2() |
| // - InputFloat3() |
| // - InputFloat4() |
| // - InputInt() |
| // - InputInt2() |
| // - InputInt3() |
| // - InputInt4() |
| // - InputDouble() |
| //------------------------------------------------------------------------- |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: InputText |
| // - InputText() |
| // - InputTextMultiline() |
| // - InputTextEx() [Internal] |
| //------------------------------------------------------------------------- |
| |
| bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) |
| { |
| IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() |
| return InputTextEx(label, buf, (int)buf_size, ImVec2(0,0), flags, callback, user_data); |
| } |
| |
| bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) |
| { |
| return InputTextEx(label, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); |
| } |
| |
| static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) |
| { |
| int line_count = 0; |
| const char* s = text_begin; |
| while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding |
| if (c == '\n') |
| line_count++; |
| s--; |
| if (s[0] != '\n' && s[0] != '\r') |
| line_count++; |
| *out_text_end = s; |
| return line_count; |
| } |
| |
| static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) |
| { |
| ImFont* font = GImGui->Font; |
| const float line_height = GImGui->FontSize; |
| const float scale = line_height / font->FontSize; |
| |
| ImVec2 text_size = ImVec2(0,0); |
| float line_width = 0.0f; |
| |
| const ImWchar* s = text_begin; |
| while (s < text_end) |
| { |
| unsigned int c = (unsigned int)(*s++); |
| if (c == '\n') |
| { |
| text_size.x = ImMax(text_size.x, line_width); |
| text_size.y += line_height; |
| line_width = 0.0f; |
| if (stop_on_new_line) |
| break; |
| continue; |
| } |
| if (c == '\r') |
| continue; |
| |
| const float char_width = font->GetCharAdvance((unsigned short)c) * scale; |
| line_width += char_width; |
| } |
| |
| if (text_size.x < line_width) |
| text_size.x = line_width; |
| |
| if (out_offset) |
| *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n |
| |
| if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n |
| text_size.y += line_height; |
| |
| if (remaining) |
| *remaining = s; |
| |
| return text_size; |
| } |
| |
| // 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) |
| namespace ImGuiStb |
| { |
| |
| static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return obj->CurLenW; } |
| static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->TextW[idx]; } |
| static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx+char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; return GImGui->Font->GetCharAdvance(c) * (GImGui->FontSize / GImGui->Font->FontSize); } |
| static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; } |
| static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; |
| static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx) |
| { |
| const ImWchar* text = obj->TextW.Data; |
| const ImWchar* text_remaining = NULL; |
| const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); |
| r->x0 = 0.0f; |
| r->x1 = size.x; |
| r->baseline_y_delta = size.y; |
| r->ymin = 0.0f; |
| r->ymax = size.y; |
| r->num_chars = (int)(text_remaining - (text + line_start_idx)); |
| } |
| |
| static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|'; } |
| static int is_word_boundary_from_right(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (is_separator( obj->TextW[idx-1] ) && !is_separator( obj->TextW[idx] ) ) : 1; } |
| static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } |
| #ifdef __APPLE__ // FIXME: Move setting to IO structure |
| static int is_word_boundary_from_left(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (!is_separator( obj->TextW[idx-1] ) && is_separator( obj->TextW[idx] ) ) : 1; } |
| static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } |
| #else |
| static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } |
| #endif |
| #define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h |
| #define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL |
| |
| static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING* obj, int pos, int n) |
| { |
| ImWchar* dst = obj->TextW.Data + pos; |
| |
| // We maintain our buffer length in both UTF-8 and wchar formats |
| obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); |
| obj->CurLenW -= n; |
| |
| // Offset remaining text |
| const ImWchar* src = obj->TextW.Data + pos + n; |
| while (ImWchar c = *src++) |
| *dst++ = c; |
| *dst = '\0'; |
| } |
| |
| static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const ImWchar* new_text, int new_text_len) |
| { |
| const bool is_resizable = (obj->UserFlags & ImGuiInputTextFlags_CallbackResize) != 0; |
| const int text_len = obj->CurLenW; |
| IM_ASSERT(pos <= text_len); |
| |
| const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); |
| if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA)) |
| return false; |
| |
| // Grow internal buffer if needed |
| if (new_text_len + text_len + 1 > obj->TextW.Size) |
| { |
| if (!is_resizable) |
| return false; |
| IM_ASSERT(text_len < obj->TextW.Size); |
| obj->TextW.resize(text_len + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1); |
| } |
| |
| ImWchar* text = obj->TextW.Data; |
| if (pos != text_len) |
| memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); |
| memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); |
| |
| obj->CurLenW += new_text_len; |
| obj->CurLenA += new_text_len_utf8; |
| obj->TextW[obj->CurLenW] = '\0'; |
| |
| return true; |
| } |
| |
| // 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) |
| #define STB_TEXTEDIT_K_LEFT 0x10000 // keyboard input to move cursor left |
| #define STB_TEXTEDIT_K_RIGHT 0x10001 // keyboard input to move cursor right |
| #define STB_TEXTEDIT_K_UP 0x10002 // keyboard input to move cursor up |
| #define STB_TEXTEDIT_K_DOWN 0x10003 // keyboard input to move cursor down |
| #define STB_TEXTEDIT_K_LINESTART 0x10004 // keyboard input to move cursor to start of line |
| #define STB_TEXTEDIT_K_LINEEND 0x10005 // keyboard input to move cursor to end of line |
| #define STB_TEXTEDIT_K_TEXTSTART 0x10006 // keyboard input to move cursor to start of text |
| #define STB_TEXTEDIT_K_TEXTEND 0x10007 // keyboard input to move cursor to end of text |
| #define STB_TEXTEDIT_K_DELETE 0x10008 // keyboard input to delete selection or character under cursor |
| #define STB_TEXTEDIT_K_BACKSPACE 0x10009 // keyboard input to delete selection or character left of cursor |
| #define STB_TEXTEDIT_K_UNDO 0x1000A // keyboard input to perform undo |
| #define STB_TEXTEDIT_K_REDO 0x1000B // keyboard input to perform redo |
| #define STB_TEXTEDIT_K_WORDLEFT 0x1000C // keyboard input to move cursor left one word |
| #define STB_TEXTEDIT_K_WORDRIGHT 0x1000D // keyboard input to move cursor right one word |
| #define STB_TEXTEDIT_K_SHIFT 0x20000 |
| |
| #define STB_TEXTEDIT_IMPLEMENTATION |
| #include "stb_textedit.h" |
| |
| } |
| |
| void ImGuiInputTextState::OnKeyPressed(int key) |
| { |
| stb_textedit_key(this, &StbState, key); |
| CursorFollow = true; |
| CursorAnimReset(); |
| } |
| |
| ImGuiInputTextCallbackData::ImGuiInputTextCallbackData() |
| { |
| memset(this, 0, sizeof(*this)); |
| } |
| |
| // Public API to manipulate UTF-8 text |
| // We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) |
| // FIXME: The existence of this rarely exercised code path is a bit of a nuisance. |
| void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) |
| { |
| IM_ASSERT(pos + bytes_count <= BufTextLen); |
| char* dst = Buf + pos; |
| const char* src = Buf + pos + bytes_count; |
| while (char c = *src++) |
| *dst++ = c; |
| *dst = '\0'; |
| |
| if (CursorPos + bytes_count >= pos) |
| CursorPos -= bytes_count; |
| else if (CursorPos >= pos) |
| CursorPos = pos; |
| SelectionStart = SelectionEnd = CursorPos; |
| BufDirty = true; |
| BufTextLen -= bytes_count; |
| } |
| |
| void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) |
| { |
| const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; |
| const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); |
| if (new_text_len + BufTextLen >= BufSize) |
| { |
| if (!is_resizable) |
| return; |
| |
| // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the midly similar code (until we remove the U16 buffer alltogether!) |
| ImGuiContext& g = *GImGui; |
| ImGuiInputTextState* edit_state = &g.InputTextState; |
| IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID); |
| IM_ASSERT(Buf == edit_state->TempBuffer.Data); |
| int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1; |
| edit_state->TempBuffer.reserve(new_buf_size + 1); |
| Buf = edit_state->TempBuffer.Data; |
| BufSize = edit_state->BufCapacityA = new_buf_size; |
| } |
| |
| if (BufTextLen != pos) |
| memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos)); |
| memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char)); |
| Buf[BufTextLen + new_text_len] = '\0'; |
| |
| if (CursorPos >= pos) |
| CursorPos += new_text_len; |
| SelectionStart = SelectionEnd = CursorPos; |
| BufDirty = true; |
| BufTextLen += new_text_len; |
| } |
| |
| // Return false to discard a character. |
| static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) |
| { |
| unsigned int c = *p_char; |
| |
| if (c < 128 && c != ' ' && !isprint((int)(c & 0xFF))) |
| { |
| bool pass = false; |
| pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); |
| pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); |
| if (!pass) |
| return false; |
| } |
| |
| if (c >= 0xE000 && c <= 0xF8FF) // Filter private Unicode range. I don't imagine anybody would want to input them. GLFW on OSX seems to send private characters for special keys like arrow keys. |
| return false; |
| |
| if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific)) |
| { |
| if (flags & ImGuiInputTextFlags_CharsDecimal) |
| if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/')) |
| return false; |
| |
| if (flags & ImGuiInputTextFlags_CharsScientific) |
| if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) |
| return false; |
| |
| if (flags & ImGuiInputTextFlags_CharsHexadecimal) |
| if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) |
| return false; |
| |
| if (flags & ImGuiInputTextFlags_CharsUppercase) |
| if (c >= 'a' && c <= 'z') |
| *p_char = (c += (unsigned int)('A'-'a')); |
| |
| if (flags & ImGuiInputTextFlags_CharsNoBlank) |
| if (ImCharIsBlankW(c)) |
| return false; |
| } |
| |
| if (flags & ImGuiInputTextFlags_CallbackCharFilter) |
| { |
| ImGuiInputTextCallbackData callback_data; |
| memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); |
| callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; |
| callback_data.EventChar = (ImWchar)c; |
| callback_data.Flags = flags; |
| callback_data.UserData = user_data; |
| if (callback(&callback_data) != 0) |
| return false; |
| *p_char = callback_data.EventChar; |
| if (!callback_data.EventChar) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Edit a string of text |
| // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". |
| // This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match |
| // Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator. |
| // - 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. |
| // - If you want to use ImGui::InputText() with std::string, see misc/stl/imgui_stl.h |
| // (FIXME: Rather messy function partly because we are doing UTF8 > u16 > UTF8 conversions on the go to more easily handle stb_textedit calls. Ideally we should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188) |
| bool ImGui::InputTextEx(const char* label, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) |
| IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiIO& io = g.IO; |
| const ImGuiStyle& style = g.Style; |
| |
| const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; |
| const bool is_editable = (flags & ImGuiInputTextFlags_ReadOnly) == 0; |
| const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; |
| const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; |
| const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; |
| if (is_resizable) |
| IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! |
| |
| if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope, |
| BeginGroup(); |
| const ImGuiID id = window->GetID(label); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? GetTextLineHeight() * 8.0f : label_size.y) + style.FramePadding.y*2.0f); // Arbitrary default of 8 lines high for multi-line |
| const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); |
| 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)); |
| |
| ImGuiWindow* draw_window = window; |
| if (is_multiline) |
| { |
| ItemAdd(total_bb, id, &frame_bb); |
| if (!BeginChildFrame(id, frame_bb.GetSize())) |
| { |
| EndChildFrame(); |
| EndGroup(); |
| return false; |
| } |
| draw_window = GetCurrentWindow(); |
| draw_window->DC.NavLayerActiveMaskNext |= draw_window->DC.NavLayerCurrentMask; // This is to ensure that EndChild() will display a navigation highlight |
| size.x -= draw_window->ScrollbarSizes.x; |
| } |
| else |
| { |
| ItemSize(total_bb, style.FramePadding.y); |
| if (!ItemAdd(total_bb, id, &frame_bb)) |
| return false; |
| } |
| const bool hovered = ItemHoverable(frame_bb, id); |
| if (hovered) |
| g.MouseCursor = ImGuiMouseCursor_TextInput; |
| |
| // Password pushes a temporary font with only a fallback glyph |
| if (is_password) |
| { |
| const ImFontGlyph* glyph = g.Font->FindGlyph('*'); |
| ImFont* password_font = &g.InputTextPasswordFont; |
| password_font->FontSize = g.Font->FontSize; |
| password_font->Scale = g.Font->Scale; |
| password_font->DisplayOffset = g.Font->DisplayOffset; |
| password_font->Ascent = g.Font->Ascent; |
| password_font->Descent = g.Font->Descent; |
| password_font->ContainerAtlas = g.Font->ContainerAtlas; |
| password_font->FallbackGlyph = glyph; |
| password_font->FallbackAdvanceX = glyph->AdvanceX; |
| IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); |
| PushFont(password_font); |
| } |
| |
| // NB: we are only allowed to access 'edit_state' if we are the active widget. |
| ImGuiInputTextState& edit_state = g.InputTextState; |
| |
| const bool focus_requested = FocusableItemRegister(window, id, (flags & (ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_AllowTabInput)) == 0); // Using completion callback disable keyboard tabbing |
| const bool focus_requested_by_code = focus_requested && (window->FocusIdxAllCounter == window->FocusIdxAllRequestCurrent); |
| const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code; |
| |
| const bool user_clicked = hovered && io.MouseClicked[0]; |
| const bool user_scrolled = is_multiline && g.ActiveId == 0 && edit_state.ID == id && g.ActiveIdPreviousFrame == draw_window->GetIDNoKeepAlive("#SCROLLY"); |
| const bool user_nav_input_start = (g.ActiveId != id) && ((g.NavInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_NavKeyboard)); |
| |
| bool clear_active_id = false; |
| |
| bool select_all = (g.ActiveId != id) && ((flags & ImGuiInputTextFlags_AutoSelectAll) != 0 || user_nav_input_start) && (!is_multiline); |
| if (focus_requested || user_clicked || user_scrolled || user_nav_input_start) |
| { |
| if (g.ActiveId != id) |
| { |
| // Start edition |
| // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) |
| // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) |
| const int prev_len_w = edit_state.CurLenW; |
| const int init_buf_len = (int)strlen(buf); |
| edit_state.TextW.resize(buf_size+1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data isn't NULL so it doesn't crash. |
| edit_state.InitialText.resize(init_buf_len + 1); // UTF-8. we use +1 to make sure that .Data isn't NULL so it doesn't crash. |
| memcpy(edit_state.InitialText.Data, buf, init_buf_len + 1); |
| const char* buf_end = NULL; |
| edit_state.CurLenW = ImTextStrFromUtf8(edit_state.TextW.Data, buf_size, buf, NULL, &buf_end); |
| edit_state.CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. |
| edit_state.CursorAnimReset(); |
| |
| // Preserve cursor position and undo/redo stack if we come back to same widget |
| // FIXME: We should probably compare the whole buffer to be on the safety side. Comparing buf (utf8) and edit_state.Text (wchar). |
| const bool recycle_state = (edit_state.ID == id) && (prev_len_w == edit_state.CurLenW); |
| if (recycle_state) |
| { |
| // Recycle existing cursor/selection/undo stack but clamp position |
| // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. |
| edit_state.CursorClamp(); |
| } |
| else |
| { |
| edit_state.ID = id; |
| edit_state.ScrollX = 0.0f; |
| stb_textedit_initialize_state(&edit_state.StbState, !is_multiline); |
| if (!is_multiline && focus_requested_by_code) |
| select_all = true; |
| } |
| if (flags & ImGuiInputTextFlags_AlwaysInsertMode) |
| edit_state.StbState.insert_mode = true; |
| if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl))) |
| select_all = true; |
| } |
| SetActiveID(id, window); |
| SetFocusID(id, window); |
| FocusWindow(window); |
| if (!is_multiline && !(flags & ImGuiInputTextFlags_CallbackHistory)) |
| g.ActiveIdAllowNavDirFlags |= ((1 << ImGuiDir_Up) | (1 << ImGuiDir_Down)); |
| } |
| else if (io.MouseClicked[0]) |
| { |
| // Release focus when we click outside |
| clear_active_id = true; |
| } |
| |
| bool value_changed = false; |
| bool enter_pressed = false; |
| int backup_current_text_length = 0; |
| |
| if (g.ActiveId == id) |
| { |
| if (!is_editable && !g.ActiveIdIsJustActivated) |
| { |
| // When read-only we always use the live data passed to the function |
| edit_state.TextW.resize(buf_size+1); |
| const char* buf_end = NULL; |
| edit_state.CurLenW = ImTextStrFromUtf8(edit_state.TextW.Data, edit_state.TextW.Size, buf, NULL, &buf_end); |
| edit_state.CurLenA = (int)(buf_end - buf); |
| edit_state.CursorClamp(); |
| } |
| |
| backup_current_text_length = edit_state.CurLenA; |
| edit_state.BufCapacityA = buf_size; |
| edit_state.UserFlags = flags; |
| edit_state.UserCallback = callback; |
| edit_state.UserCallbackData = callback_user_data; |
| |
| // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. |
| // Down the line we should have a cleaner library-wide concept of Selected vs Active. |
| g.ActiveIdAllowOverlap = !io.MouseDown[0]; |
| g.WantTextInputNextFrame = 1; |
| |
| // Edit in progress |
| const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + edit_state.ScrollX; |
| const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize*0.5f)); |
| |
| const bool is_osx = io.ConfigMacOSXBehaviors; |
| if (select_all || (hovered && !is_osx && io.MouseDoubleClicked[0])) |
| { |
| edit_state.SelectAll(); |
| edit_state.SelectedAllMouseLock = true; |
| } |
| else if (hovered && is_osx && io.MouseDoubleClicked[0]) |
| { |
| // Double-click select a word only, OS X style (by simulating keystrokes) |
| edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); |
| edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); |
| } |
| else if (io.MouseClicked[0] && !edit_state.SelectedAllMouseLock) |
| { |
| if (hovered) |
| { |
| stb_textedit_click(&edit_state, &edit_state.StbState, mouse_x, mouse_y); |
| edit_state.CursorAnimReset(); |
| } |
| } |
| else if (io.MouseDown[0] && !edit_state.SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) |
| { |
| stb_textedit_drag(&edit_state, &edit_state.StbState, mouse_x, mouse_y); |
| edit_state.CursorAnimReset(); |
| edit_state.CursorFollow = true; |
| } |
| if (edit_state.SelectedAllMouseLock && !io.MouseDown[0]) |
| edit_state.SelectedAllMouseLock = false; |
| |
| if (io.InputCharacters[0]) |
| { |
| // Process text input (before we check for Return because using some IME will effectively send a Return?) |
| // 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. |
| bool ignore_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper); |
| if (!ignore_inputs && is_editable && !user_nav_input_start) |
| for (int n = 0; n < IM_ARRAYSIZE(io.InputCharacters) && io.InputCharacters[n]; n++) |
| { |
| // Insert character if they pass filtering |
| unsigned int c = (unsigned int)io.InputCharacters[n]; |
| if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) |
| edit_state.OnKeyPressed((int)c); |
| } |
| |
| // Consume characters |
| memset(g.IO.InputCharacters, 0, sizeof(g.IO.InputCharacters)); |
| } |
| } |
| |
| bool cancel_edit = false; |
| if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) |
| { |
| // Handle key-presses |
| const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); |
| const bool is_osx = io.ConfigMacOSXBehaviors; |
| const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl |
| const bool is_osx_shift_shortcut = is_osx && io.KeySuper && io.KeyShift && !io.KeyCtrl && !io.KeyAlt; |
| const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl |
| const bool is_startend_key_down = is_osx && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End |
| const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper; |
| const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper; |
| |
| const bool is_cut = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && is_editable && !is_password && (!is_multiline || edit_state.HasSelection()); |
| const bool is_copy = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || edit_state.HasSelection()); |
| const bool is_paste = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && is_editable; |
| const bool is_undo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Z)) && is_editable && is_undoable); |
| const bool is_redo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Y)) || (is_osx_shift_shortcut && IsKeyPressedMap(ImGuiKey_Z))) && is_editable && is_undoable; |
| |
| if (IsKeyPressedMap(ImGuiKey_LeftArrow)) { edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_RightArrow)) { edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetWindowScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else edit_state.OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_Home)) { edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_End)) { edit_state.OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_Delete) && is_editable) { edit_state.OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); } |
| else if (IsKeyPressedMap(ImGuiKey_Backspace) && is_editable) |
| { |
| if (!edit_state.HasSelection()) |
| { |
| if (is_wordmove_key_down) edit_state.OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT|STB_TEXTEDIT_K_SHIFT); |
| else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl) edit_state.OnKeyPressed(STB_TEXTEDIT_K_LINESTART|STB_TEXTEDIT_K_SHIFT); |
| } |
| edit_state.OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); |
| } |
| else if (IsKeyPressedMap(ImGuiKey_Enter)) |
| { |
| bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; |
| if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) |
| { |
| enter_pressed = clear_active_id = true; |
| } |
| else if (is_editable) |
| { |
| unsigned int c = '\n'; // Insert new line |
| if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) |
| edit_state.OnKeyPressed((int)c); |
| } |
| } |
| else if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !io.KeyCtrl && !io.KeyShift && !io.KeyAlt && is_editable) |
| { |
| unsigned int c = '\t'; // Insert TAB |
| if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) |
| edit_state.OnKeyPressed((int)c); |
| } |
| else if (IsKeyPressedMap(ImGuiKey_Escape)) |
| { |
| clear_active_id = cancel_edit = true; |
| } |
| else if (is_undo || is_redo) |
| { |
| edit_state.OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO); |
| edit_state.ClearSelection(); |
| } |
| else if (is_shortcut_key && IsKeyPressedMap(ImGuiKey_A)) |
| { |
| edit_state.SelectAll(); |
| edit_state.CursorFollow = true; |
| } |
| else if (is_cut || is_copy) |
| { |
| // Cut, Copy |
| if (io.SetClipboardTextFn) |
| { |
| const int ib = edit_state.HasSelection() ? ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end) : 0; |
| const int ie = edit_state.HasSelection() ? ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end) : edit_state.CurLenW; |
| edit_state.TempBuffer.resize((ie-ib) * 4 + 1); |
| ImTextStrToUtf8(edit_state.TempBuffer.Data, edit_state.TempBuffer.Size, edit_state.TextW.Data+ib, edit_state.TextW.Data+ie); |
| SetClipboardText(edit_state.TempBuffer.Data); |
| } |
| if (is_cut) |
| { |
| if (!edit_state.HasSelection()) |
| edit_state.SelectAll(); |
| edit_state.CursorFollow = true; |
| stb_textedit_cut(&edit_state, &edit_state.StbState); |
| } |
| } |
| else if (is_paste) |
| { |
| if (const char* clipboard = GetClipboardText()) |
| { |
| // Filter pasted buffer |
| const int clipboard_len = (int)strlen(clipboard); |
| ImWchar* clipboard_filtered = (ImWchar*)ImGui::MemAlloc((clipboard_len+1) * sizeof(ImWchar)); |
| int clipboard_filtered_len = 0; |
| for (const char* s = clipboard; *s; ) |
| { |
| unsigned int c; |
| s += ImTextCharFromUtf8(&c, s, NULL); |
| if (c == 0) |
| break; |
| if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, callback_user_data)) |
| continue; |
| clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; |
| } |
| clipboard_filtered[clipboard_filtered_len] = 0; |
| if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation |
| { |
| stb_textedit_paste(&edit_state, &edit_state.StbState, clipboard_filtered, clipboard_filtered_len); |
| edit_state.CursorFollow = true; |
| } |
| ImGui::MemFree(clipboard_filtered); |
| } |
| } |
| } |
| |
| if (g.ActiveId == id) |
| { |
| const char* apply_new_text = NULL; |
| int apply_new_text_length = 0; |
| if (cancel_edit) |
| { |
| // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. |
| if (is_editable && strcmp(buf, edit_state.InitialText.Data) != 0) |
| { |
| apply_new_text = edit_state.InitialText.Data; |
| apply_new_text_length = edit_state.InitialText.Size - 1; |
| } |
| } |
| |
| // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. |
| // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage. |
| bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); |
| if (apply_edit_back_to_user_buffer) |
| { |
| // Apply new value immediately - copy modified buffer back |
| // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer |
| // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. |
| // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. |
| if (is_editable) |
| { |
| edit_state.TempBuffer.resize(edit_state.TextW.Size * 4 + 1); |
| ImTextStrToUtf8(edit_state.TempBuffer.Data, edit_state.TempBuffer.Size, edit_state.TextW.Data, NULL); |
| } |
| |
| // User callback |
| if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways)) != 0) |
| { |
| IM_ASSERT(callback != NULL); |
| |
| // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. |
| ImGuiInputTextFlags event_flag = 0; |
| ImGuiKey event_key = ImGuiKey_COUNT; |
| if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab)) |
| { |
| event_flag = ImGuiInputTextFlags_CallbackCompletion; |
| event_key = ImGuiKey_Tab; |
| } |
| else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow)) |
| { |
| event_flag = ImGuiInputTextFlags_CallbackHistory; |
| event_key = ImGuiKey_UpArrow; |
| } |
| else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow)) |
| { |
| event_flag = ImGuiInputTextFlags_CallbackHistory; |
| event_key = ImGuiKey_DownArrow; |
| } |
| else if (flags & ImGuiInputTextFlags_CallbackAlways) |
| event_flag = ImGuiInputTextFlags_CallbackAlways; |
| |
| if (event_flag) |
| { |
| ImGuiInputTextCallbackData callback_data; |
| memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); |
| callback_data.EventFlag = event_flag; |
| callback_data.Flags = flags; |
| callback_data.UserData = callback_user_data; |
| |
| callback_data.EventKey = event_key; |
| callback_data.Buf = edit_state.TempBuffer.Data; |
| callback_data.BufTextLen = edit_state.CurLenA; |
| callback_data.BufSize = edit_state.BufCapacityA; |
| callback_data.BufDirty = false; |
| |
| // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) |
| ImWchar* text = edit_state.TextW.Data; |
| const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.cursor); |
| const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_start); |
| const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + edit_state.StbState.select_end); |
| |
| // Call user code |
| callback(&callback_data); |
| |
| // Read back what user may have modified |
| IM_ASSERT(callback_data.Buf == edit_state.TempBuffer.Data); // Invalid to modify those fields |
| IM_ASSERT(callback_data.BufSize == edit_state.BufCapacityA); |
| IM_ASSERT(callback_data.Flags == flags); |
| if (callback_data.CursorPos != utf8_cursor_pos) { edit_state.StbState.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); edit_state.CursorFollow = true; } |
| if (callback_data.SelectionStart != utf8_selection_start) { edit_state.StbState.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); } |
| if (callback_data.SelectionEnd != utf8_selection_end) { edit_state.StbState.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } |
| if (callback_data.BufDirty) |
| { |
| IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! |
| if (callback_data.BufTextLen > backup_current_text_length && is_resizable) |
| edit_state.TextW.resize(edit_state.TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); |
| edit_state.CurLenW = ImTextStrFromUtf8(edit_state.TextW.Data, edit_state.TextW.Size, callback_data.Buf, NULL); |
| edit_state.CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() |
| edit_state.CursorAnimReset(); |
| } |
| } |
| } |
| |
| // Will copy result string if modified |
| if (is_editable && strcmp(edit_state.TempBuffer.Data, buf) != 0) |
| { |
| apply_new_text = edit_state.TempBuffer.Data; |
| apply_new_text_length = edit_state.CurLenA; |
| } |
| } |
| |
| // Copy result to user buffer |
| if (apply_new_text) |
| { |
| IM_ASSERT(apply_new_text_length >= 0); |
| if (backup_current_text_length != apply_new_text_length && is_resizable) |
| { |
| ImGuiInputTextCallbackData callback_data; |
| callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; |
| callback_data.Flags = flags; |
| callback_data.Buf = buf; |
| callback_data.BufTextLen = apply_new_text_length; |
| callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); |
| callback_data.UserData = callback_user_data; |
| callback(&callback_data); |
| buf = callback_data.Buf; |
| buf_size = callback_data.BufSize; |
| apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); |
| IM_ASSERT(apply_new_text_length <= buf_size); |
| } |
| |
| // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. |
| ImStrncpy(buf, edit_state.TempBuffer.Data, ImMin(apply_new_text_length + 1, buf_size)); |
| value_changed = true; |
| } |
| |
| // Clear temporary user storage |
| edit_state.UserFlags = 0; |
| edit_state.UserCallback = NULL; |
| edit_state.UserCallbackData = NULL; |
| } |
| |
| // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) |
| if (clear_active_id && g.ActiveId == id) |
| ClearActiveID(); |
| |
| // Render |
| // Select which buffer we are going to display. When ImGuiInputTextFlags_NoLiveEdit is set 'buf' might still be the old value. We set buf to NULL to prevent accidental usage from now on. |
| const char* buf_display = (g.ActiveId == id && is_editable) ? edit_state.TempBuffer.Data : buf; buf = NULL; |
| |
| // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line |
| // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether. |
| // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash. |
| const int buf_display_max_length = 2 * 1024 * 1024; |
| |
| if (!is_multiline) |
| { |
| RenderNavHighlight(frame_bb, id); |
| RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); |
| } |
| |
| const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + size.x, frame_bb.Min.y + size.y); // Not using frame_bb.Max because we have adjusted size |
| ImVec2 render_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; |
| ImVec2 text_size(0.f, 0.f); |
| const bool is_currently_scrolling = (edit_state.ID == id && is_multiline && g.ActiveId == draw_window->GetIDNoKeepAlive("#SCROLLY")); |
| if (g.ActiveId == id || is_currently_scrolling) |
| { |
| edit_state.CursorAnim += io.DeltaTime; |
| |
| // This is going to be messy. We need to: |
| // - Display the text (this alone can be more easily clipped) |
| // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) |
| // - Measure text height (for scrollbar) |
| // 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) |
| // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. |
| const ImWchar* text_begin = edit_state.TextW.Data; |
| ImVec2 cursor_offset, select_start_offset; |
| |
| { |
| // Count lines + find lines numbers straddling 'cursor' and 'select_start' position. |
| const ImWchar* searches_input_ptr[2]; |
| searches_input_ptr[0] = text_begin + edit_state.StbState.cursor; |
| searches_input_ptr[1] = NULL; |
| int searches_remaining = 1; |
| int searches_result_line_number[2] = { -1, -999 }; |
| if (edit_state.StbState.select_start != edit_state.StbState.select_end) |
| { |
| searches_input_ptr[1] = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); |
| searches_result_line_number[1] = -1; |
| searches_remaining++; |
| } |
| |
| // Iterate all lines to find our line numbers |
| // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. |
| searches_remaining += is_multiline ? 1 : 0; |
| int line_count = 0; |
| for (const ImWchar* s = text_begin; *s != 0; s++) |
| if (*s == '\n') |
| { |
| line_count++; |
| if (searches_result_line_number[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_number[0] = line_count; if (--searches_remaining <= 0) break; } |
| if (searches_result_line_number[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_number[1] = line_count; if (--searches_remaining <= 0) break; } |
| } |
| line_count++; |
| if (searches_result_line_number[0] == -1) searches_result_line_number[0] = line_count; |
| if (searches_result_line_number[1] == -1) searches_result_line_number[1] = line_count; |
| |
| // Calculate 2d position by finding the beginning of the line and measuring distance |
| cursor_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; |
| cursor_offset.y = searches_result_line_number[0] * g.FontSize; |
| if (searches_result_line_number[1] >= 0) |
| { |
| select_start_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; |
| select_start_offset.y = searches_result_line_number[1] * g.FontSize; |
| } |
| |
| // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) |
| if (is_multiline) |
| text_size = ImVec2(size.x, line_count * g.FontSize); |
| } |
| |
| // Scroll |
| if (edit_state.CursorFollow) |
| { |
| // Horizontal scroll in chunks of quarter width |
| if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) |
| { |
| const float scroll_increment_x = size.x * 0.25f; |
| if (cursor_offset.x < edit_state.ScrollX) |
| edit_state.ScrollX = (float)(int)ImMax(0.0f, cursor_offset.x - scroll_increment_x); |
| else if (cursor_offset.x - size.x >= edit_state.ScrollX) |
| edit_state.ScrollX = (float)(int)(cursor_offset.x - size.x + scroll_increment_x); |
| } |
| else |
| { |
| edit_state.ScrollX = 0.0f; |
| } |
| |
| // Vertical scroll |
| if (is_multiline) |
| { |
| float scroll_y = draw_window->Scroll.y; |
| if (cursor_offset.y - g.FontSize < scroll_y) |
| scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); |
| else if (cursor_offset.y - size.y >= scroll_y) |
| scroll_y = cursor_offset.y - size.y; |
| draw_window->DC.CursorPos.y += (draw_window->Scroll.y - scroll_y); // To avoid a frame of lag |
| draw_window->Scroll.y = scroll_y; |
| render_pos.y = draw_window->DC.CursorPos.y; |
| } |
| } |
| edit_state.CursorFollow = false; |
| const ImVec2 render_scroll = ImVec2(edit_state.ScrollX, 0.0f); |
| |
| // Draw selection |
| if (edit_state.StbState.select_start != edit_state.StbState.select_end) |
| { |
| const ImWchar* text_selected_begin = text_begin + ImMin(edit_state.StbState.select_start, edit_state.StbState.select_end); |
| const ImWchar* text_selected_end = text_begin + ImMax(edit_state.StbState.select_start, edit_state.StbState.select_end); |
| |
| 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. |
| float bg_offy_dn = is_multiline ? 0.0f : 2.0f; |
| ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg); |
| ImVec2 rect_pos = render_pos + select_start_offset - render_scroll; |
| for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) |
| { |
| if (rect_pos.y > clip_rect.w + g.FontSize) |
| break; |
| if (rect_pos.y < clip_rect.y) |
| { |
| while (p < text_selected_end) |
| if (*p++ == '\n') |
| break; |
| } |
| else |
| { |
| ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true); |
| if (rect_size.x <= 0.0f) rect_size.x = (float)(int)(g.Font->GetCharAdvance((unsigned short)' ') * 0.50f); // So we can see selected empty lines |
| ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos +ImVec2(rect_size.x, bg_offy_dn)); |
| rect.ClipWith(clip_rect); |
| if (rect.Overlaps(clip_rect)) |
| draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); |
| } |
| rect_pos.x = render_pos.x - render_scroll.x; |
| rect_pos.y += g.FontSize; |
| } |
| } |
| |
| const int buf_display_len = edit_state.CurLenA; |
| if (is_multiline || buf_display_len < buf_display_max_length) |
| draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos - render_scroll, GetColorU32(ImGuiCol_Text), buf_display, buf_display + buf_display_len, 0.0f, is_multiline ? NULL : &clip_rect); |
| |
| // Draw blinking cursor |
| bool cursor_is_visible = (!g.IO.ConfigCursorBlink) || (g.InputTextState.CursorAnim <= 0.0f) || ImFmod(g.InputTextState.CursorAnim, 1.20f) <= 0.80f; |
| ImVec2 cursor_screen_pos = render_pos + cursor_offset - render_scroll; |
| 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); |
| if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) |
| draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); |
| |
| // 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.) |
| if (is_editable) |
| g.PlatformImePos = ImVec2(cursor_screen_pos.x - 1, cursor_screen_pos.y - g.FontSize); |
| } |
| else |
| { |
| // Render text only |
| const char* buf_end = NULL; |
| if (is_multiline) |
| text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_end) * g.FontSize); // We don't need width |
| else |
| buf_end = buf_display + strlen(buf_display); |
| if (is_multiline || (buf_end - buf_display) < buf_display_max_length) |
| draw_window->DrawList->AddText(g.Font, g.FontSize, render_pos, GetColorU32(ImGuiCol_Text), buf_display, buf_end, 0.0f, is_multiline ? NULL : &clip_rect); |
| } |
| |
| if (is_multiline) |
| { |
| Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line |
| EndChildFrame(); |
| EndGroup(); |
| } |
| |
| if (is_password) |
| PopFont(); |
| |
| // Log as text |
| if (g.LogEnabled && !is_password) |
| LogRenderedText(&render_pos, buf_display, NULL); |
| |
| if (label_size.x > 0) |
| RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); |
| |
| if (value_changed) |
| MarkItemEdited(id); |
| |
| if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) |
| return enter_pressed; |
| else |
| return value_changed; |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Color Editor / Picker |
| // - ColorEdit3() |
| // - ColorEdit4() |
| // - ColorPicker3() |
| // - RenderColorRectWithAlphaCheckerboard() [Internal] |
| // - ColorPicker4() |
| // - ColorButton() |
| // - SetColorEditOptions() |
| // - ColorTooltip() [Internal] |
| // - ColorEditOptionsPopup() [Internal] |
| // - ColorPickerOptionsPopup() [Internal] |
| //------------------------------------------------------------------------- |
| |
| bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) |
| { |
| return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); |
| } |
| |
| // Edit colors components (each component in 0.0f..1.0f range). |
| // See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. |
| // With typical options: Left-click on colored 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. |
| bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const float square_sz = GetFrameHeight(); |
| const float w_extra = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); |
| const float w_items_all = CalcItemWidth() - w_extra; |
| const char* label_display_end = FindRenderedTextEnd(label); |
| |
| BeginGroup(); |
| PushID(label); |
| |
| // If we're not showing any slider there's no point in doing any HSV conversions |
| const ImGuiColorEditFlags flags_untouched = flags; |
| if (flags & ImGuiColorEditFlags_NoInputs) |
| flags = (flags & (~ImGuiColorEditFlags__InputsMask)) | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_NoOptions; |
| |
| // Context menu: display and modify options (before defaults are applied) |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| ColorEditOptionsPopup(col, flags); |
| |
| // Read stored options |
| if (!(flags & ImGuiColorEditFlags__InputsMask)) |
| flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputsMask); |
| if (!(flags & ImGuiColorEditFlags__DataTypeMask)) |
| flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask); |
| if (!(flags & ImGuiColorEditFlags__PickerMask)) |
| flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask); |
| flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask)); |
| |
| const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; |
| const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; |
| const int components = alpha ? 4 : 3; |
| |
| // Convert to the formats we need |
| float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; |
| if (flags & ImGuiColorEditFlags_HSV) |
| ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); |
| 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]) }; |
| |
| bool value_changed = false; |
| bool value_changed_as_float = false; |
| |
| if ((flags & (ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) |
| { |
| // RGB/HSV 0..255 Sliders |
| const float w_item_one = ImMax(1.0f, (float)(int)((w_items_all - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); |
| const float w_item_last = ImMax(1.0f, (float)(int)(w_items_all - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); |
| |
| const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); |
| const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; |
| const char* fmt_table_int[3][4] = |
| { |
| { "%3d", "%3d", "%3d", "%3d" }, // Short display |
| { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA |
| { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA |
| }; |
| const char* fmt_table_float[3][4] = |
| { |
| { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display |
| { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA |
| { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA |
| }; |
| const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_HSV) ? 2 : 1; |
| |
| PushItemWidth(w_item_one); |
| for (int n = 0; n < components; n++) |
| { |
| if (n > 0) |
| SameLine(0, style.ItemInnerSpacing.x); |
| if (n + 1 == components) |
| PushItemWidth(w_item_last); |
| if (flags & ImGuiColorEditFlags_Float) |
| value_changed = value_changed_as_float = value_changed | DragFloat(ids[n], &f[n], 1.0f/255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); |
| else |
| value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| OpenPopupOnItemClick("context"); |
| } |
| PopItemWidth(); |
| PopItemWidth(); |
| } |
| else if ((flags & ImGuiColorEditFlags_HEX) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) |
| { |
| // RGB Hexadecimal Input |
| char buf[64]; |
| if (alpha) |
| 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)); |
| else |
| ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255)); |
| PushItemWidth(w_items_all); |
| if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) |
| { |
| value_changed = true; |
| char* p = buf; |
| while (*p == '#' || ImCharIsBlankA(*p)) |
| p++; |
| i[0] = i[1] = i[2] = i[3] = 0; |
| if (alpha) |
| 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) |
| else |
| sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); |
| } |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| OpenPopupOnItemClick("context"); |
| PopItemWidth(); |
| } |
| |
| ImGuiWindow* picker_active_window = NULL; |
| if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) |
| { |
| if (!(flags & ImGuiColorEditFlags_NoInputs)) |
| SameLine(0, style.ItemInnerSpacing.x); |
| |
| const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); |
| if (ColorButton("##ColorButton", col_v4, flags)) |
| { |
| if (!(flags & ImGuiColorEditFlags_NoPicker)) |
| { |
| // Store current color and open a picker |
| g.ColorPickerRef = col_v4; |
| OpenPopup("picker"); |
| SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y)); |
| } |
| } |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| OpenPopupOnItemClick("context"); |
| |
| if (BeginPopup("picker")) |
| { |
| picker_active_window = g.CurrentWindow; |
| if (label != label_display_end) |
| { |
| TextUnformatted(label, label_display_end); |
| Separator(); |
| } |
| ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; |
| ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; |
| PushItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? |
| value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); |
| PopItemWidth(); |
| EndPopup(); |
| } |
| } |
| |
| if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) |
| { |
| SameLine(0, style.ItemInnerSpacing.x); |
| TextUnformatted(label, label_display_end); |
| } |
| |
| // Convert back |
| if (picker_active_window == NULL) |
| { |
| if (!value_changed_as_float) |
| for (int n = 0; n < 4; n++) |
| f[n] = i[n] / 255.0f; |
| if (flags & ImGuiColorEditFlags_HSV) |
| ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); |
| if (value_changed) |
| { |
| col[0] = f[0]; |
| col[1] = f[1]; |
| col[2] = f[2]; |
| if (alpha) |
| col[3] = f[3]; |
| } |
| } |
| |
| PopID(); |
| EndGroup(); |
| |
| // Drag and Drop Target |
| // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. |
| if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) |
| { |
| if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) |
| { |
| memcpy((float*)col, payload->Data, sizeof(float) * 3); |
| value_changed = true; |
| } |
| if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) |
| { |
| memcpy((float*)col, payload->Data, sizeof(float) * components); |
| value_changed = true; |
| } |
| EndDragDropTarget(); |
| } |
| |
| // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). |
| if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) |
| window->DC.LastItemId = g.ActiveId; |
| |
| if (value_changed) |
| MarkItemEdited(window->DC.LastItemId); |
| |
| return value_changed; |
| } |
| |
| bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) |
| { |
| float col4[4] = { col[0], col[1], col[2], 1.0f }; |
| if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) |
| return false; |
| col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; |
| return true; |
| } |
| |
| static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) |
| { |
| float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; |
| int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); |
| int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); |
| int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); |
| return IM_COL32(r, g, b, 0xFF); |
| } |
| |
| // Helper for ColorPicker4() |
| // NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. |
| // I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether. |
| void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) |
| { |
| ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204,204,204,255), col)); |
| ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128,128,128,255), col)); |
| window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags); |
| |
| int yi = 0; |
| for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) |
| { |
| float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); |
| if (y2 <= y1) |
| continue; |
| for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) |
| { |
| float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); |
| if (x2 <= x1) |
| continue; |
| int rounding_corners_flags_cell = 0; |
| if (y1 <= p_min.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; } |
| if (y2 >= p_max.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; } |
| rounding_corners_flags_cell &= rounding_corners_flags; |
| window->DrawList->AddRectFilled(ImVec2(x1,y1), ImVec2(x2,y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell); |
| } |
| } |
| } |
| else |
| { |
| window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags); |
| } |
| } |
| |
| // Helper for ColorPicker4() |
| static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w) |
| { |
| 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_BLACK); |
| ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32_WHITE); |
| 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_BLACK); |
| ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32_WHITE); |
| } |
| |
| // Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. |
| // 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..) |
| bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) |
| { |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = GetCurrentWindow(); |
| ImDrawList* draw_list = window->DrawList; |
| |
| ImGuiStyle& style = g.Style; |
| ImGuiIO& io = g.IO; |
| |
| PushID(label); |
| BeginGroup(); |
| |
| if (!(flags & ImGuiColorEditFlags_NoSidePreview)) |
| flags |= ImGuiColorEditFlags_NoSmallPreview; |
| |
| // Context menu: display and store options. |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| ColorPickerOptionsPopup(col, flags); |
| |
| // Read stored options |
| if (!(flags & ImGuiColorEditFlags__PickerMask)) |
| flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask; |
| IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check that only 1 is selected |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); |
| |
| // Setup |
| int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; |
| bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); |
| ImVec2 picker_pos = window->DC.CursorPos; |
| float square_sz = GetFrameHeight(); |
| float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars |
| float sv_picker_size = ImMax(bars_width * 1, CalcItemWidth() - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box |
| float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; |
| float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; |
| float bars_triangles_half_sz = (float)(int)(bars_width * 0.20f); |
| |
| float backup_initial_col[4]; |
| memcpy(backup_initial_col, col, components * sizeof(float)); |
| |
| float wheel_thickness = sv_picker_size * 0.08f; |
| float wheel_r_outer = sv_picker_size * 0.50f; |
| float wheel_r_inner = wheel_r_outer - wheel_thickness; |
| ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size*0.5f); |
| |
| // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. |
| float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); |
| ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. |
| ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. |
| ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. |
| |
| float H,S,V; |
| ColorConvertRGBtoHSV(col[0], col[1], col[2], H, S, V); |
| |
| bool value_changed = false, value_changed_h = false, value_changed_sv = false; |
| |
| PushItemFlag(ImGuiItemFlags_NoNav, true); |
| if (flags & ImGuiColorEditFlags_PickerHueWheel) |
| { |
| // Hue wheel + SV triangle logic |
| InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); |
| if (IsItemActive()) |
| { |
| ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; |
| ImVec2 current_off = g.IO.MousePos - wheel_center; |
| float initial_dist2 = ImLengthSqr(initial_off); |
| if (initial_dist2 >= (wheel_r_inner-1)*(wheel_r_inner-1) && initial_dist2 <= (wheel_r_outer+1)*(wheel_r_outer+1)) |
| { |
| // Interactive with Hue wheel |
| H = ImAtan2(current_off.y, current_off.x) / IM_PI*0.5f; |
| if (H < 0.0f) |
| H += 1.0f; |
| value_changed = value_changed_h = true; |
| } |
| float cos_hue_angle = ImCos(-H * 2.0f * IM_PI); |
| float sin_hue_angle = ImSin(-H * 2.0f * IM_PI); |
| if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) |
| { |
| // Interacting with SV triangle |
| ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); |
| if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) |
| current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); |
| float uu, vv, ww; |
| ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); |
| V = ImClamp(1.0f - vv, 0.0001f, 1.0f); |
| S = ImClamp(uu / V, 0.0001f, 1.0f); |
| value_changed = value_changed_sv = true; |
| } |
| } |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| OpenPopupOnItemClick("context"); |
| } |
| else if (flags & ImGuiColorEditFlags_PickerHueBar) |
| { |
| // SV rectangle logic |
| InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); |
| if (IsItemActive()) |
| { |
| S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size-1)); |
| V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); |
| value_changed = value_changed_sv = true; |
| } |
| if (!(flags & ImGuiColorEditFlags_NoOptions)) |
| OpenPopupOnItemClick("context"); |
| |
| // Hue bar logic |
| SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); |
| InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); |
| if (IsItemActive()) |
| { |
| H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); |
| value_changed = value_changed_h = true; |
| } |
| } |
| |
| // Alpha bar logic |
| if (alpha_bar) |
| { |
| SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); |
| InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); |
| if (IsItemActive()) |
| { |
| col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); |
| value_changed = true; |
| } |
| } |
| PopItemFlag(); // ImGuiItemFlags_NoNav |
| |
| if (!(flags & ImGuiColorEditFlags_NoSidePreview)) |
| { |
| SameLine(0, style.ItemInnerSpacing.x); |
| BeginGroup(); |
| } |
| |
| if (!(flags & ImGuiColorEditFlags_NoLabel)) |
| { |
| const char* label_display_end = FindRenderedTextEnd(label); |
| if (label != label_display_end) |
| { |
| if ((flags & ImGuiColorEditFlags_NoSidePreview)) |
| SameLine(0, style.ItemInnerSpacing.x); |
| TextUnformatted(label, label_display_end); |
| } |
| } |
| |
| if (!(flags & ImGuiColorEditFlags_NoSidePreview)) |
| { |
| PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); |
| ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); |
| if ((flags & ImGuiColorEditFlags_NoLabel)) |
| Text("Current"); |
| ColorButton("##current", col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2)); |
| if (ref_col != NULL) |
| { |
| Text("Original"); |
| ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); |
| if (ColorButton("##original", ref_col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2))) |
| { |
| memcpy(col, ref_col, components * sizeof(float)); |
| value_changed = true; |
| } |
| } |
| PopItemFlag(); |
| EndGroup(); |
| } |
| |
| // Convert back color to RGB |
| if (value_changed_h || value_changed_sv) |
| ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10*1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]); |
| |
| // R,G,B and H,S,V slider color editor |
| bool value_changed_fix_hue_wrap = false; |
| if ((flags & ImGuiColorEditFlags_NoInputs) == 0) |
| { |
| PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); |
| ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; |
| ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; |
| if (flags & ImGuiColorEditFlags_RGB || (flags & ImGuiColorEditFlags__InputsMask) == 0) |
| if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_RGB)) |
| { |
| // FIXME: Hackily differenciating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. |
| // 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) |
| value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap); |
| value_changed = true; |
| } |
| if (flags & ImGuiColorEditFlags_HSV || (flags & ImGuiColorEditFlags__InputsMask) == 0) |
| value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_HSV); |
| if (flags & ImGuiColorEditFlags_HEX || (flags & ImGuiColorEditFlags__InputsMask) == 0) |
| value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_HEX); |
| PopItemWidth(); |
| } |
| |
| // Try to cancel hue wrap (after ColorEdit4 call), if any |
| if (value_changed_fix_hue_wrap) |
| { |
| float new_H, new_S, new_V; |
| ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); |
| if (new_H <= 0 && H > 0) |
| { |
| if (new_V <= 0 && V != new_V) |
| ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); |
| else if (new_S <= 0) |
| ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); |
| } |
| } |
| |
| ImVec4 hue_color_f(1, 1, 1, 1); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); |
| ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); |
| ImU32 col32_no_alpha = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 1.0f)); |
| |
| const ImU32 hue_colors[6+1] = { IM_COL32(255,0,0,255), IM_COL32(255,255,0,255), IM_COL32(0,255,0,255), IM_COL32(0,255,255,255), IM_COL32(0,0,255,255), IM_COL32(255,0,255,255), IM_COL32(255,0,0,255) }; |
| ImVec2 sv_cursor_pos; |
| |
| if (flags & ImGuiColorEditFlags_PickerHueWheel) |
| { |
| // Render Hue Wheel |
| const float aeps = 1.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). |
| const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); |
| for (int n = 0; n < 6; n++) |
| { |
| const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; |
| const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; |
| const int vert_start_idx = draw_list->VtxBuffer.Size; |
| draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); |
| draw_list->PathStroke(IM_COL32_WHITE, false, wheel_thickness); |
| const int vert_end_idx = draw_list->VtxBuffer.Size; |
| |
| // Paint colors over existing vertices |
| ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner); |
| ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner); |
| ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, hue_colors[n], hue_colors[n+1]); |
| } |
| |
| // Render Cursor + preview on Hue Wheel |
| float cos_hue_angle = ImCos(H * 2.0f * IM_PI); |
| float sin_hue_angle = ImSin(H * 2.0f * IM_PI); |
| 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); |
| float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; |
| int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32); |
| draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); |
| draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad+1, IM_COL32(128,128,128,255), hue_cursor_segments); |
| draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, IM_COL32_WHITE, hue_cursor_segments); |
| |
| // Render SV triangle (rotated according to hue) |
| ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); |
| ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); |
| ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); |
| ImVec2 uv_white = GetFontTexUvWhitePixel(); |
| draw_list->PrimReserve(6, 6); |
| draw_list->PrimVtx(tra, uv_white, hue_color32); |
| draw_list->PrimVtx(trb, uv_white, hue_color32); |
| draw_list->PrimVtx(trc, uv_white, IM_COL32_WHITE); |
| draw_list->PrimVtx(tra, uv_white, IM_COL32_BLACK_TRANS); |
| draw_list->PrimVtx(trb, uv_white, IM_COL32_BLACK); |
| draw_list->PrimVtx(trc, uv_white, IM_COL32_BLACK_TRANS); |
| draw_list->AddTriangle(tra, trb, trc, IM_COL32(128,128,128,255), 1.5f); |
| sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); |
| } |
| else if (flags & ImGuiColorEditFlags_PickerHueBar) |
| { |
| // Render SV Square |
| draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_WHITE, hue_color32, hue_color32, IM_COL32_WHITE); |
| draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32_BLACK, IM_COL32_BLACK); |
| RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), 0.0f); |
| sv_cursor_pos.x = ImClamp((float)(int)(picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much |
| sv_cursor_pos.y = ImClamp((float)(int)(picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); |
| |
| // Render Hue Bar |
| for (int i = 0; i < 6; ++i) |
| 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)), hue_colors[i], hue_colors[i], hue_colors[i + 1], hue_colors[i + 1]); |
| float bar0_line_y = (float)(int)(picker_pos.y + H * sv_picker_size + 0.5f); |
| RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); |
| 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); |
| } |
| |
| // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) |
| float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; |
| draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, col32_no_alpha, 12); |
| draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, IM_COL32(128,128,128,255), 12); |
| draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, IM_COL32_WHITE, 12); |
| |
| // Render alpha bar |
| if (alpha_bar) |
| { |
| float alpha = ImSaturate(col[3]); |
| ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); |
| RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, IM_COL32(0,0,0,0), bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); |
| draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, col32_no_alpha, col32_no_alpha, col32_no_alpha & ~IM_COL32_A_MASK, col32_no_alpha & ~IM_COL32_A_MASK); |
| float bar1_line_y = (float)(int)(picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f); |
| RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); |
| 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); |
| } |
| |
| EndGroup(); |
| |
| if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0) |
| value_changed = false; |
| if (value_changed) |
| MarkItemEdited(window->DC.LastItemId); |
| |
| PopID(); |
| |
| return value_changed; |
| } |
| |
| // A little colored square. Return true when clicked. |
| // FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. |
| // 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. |
| bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiID id = window->GetID(desc_id); |
| float default_size = GetFrameHeight(); |
| if (size.x == 0.0f) |
| size.x = default_size; |
| if (size.y == 0.0f) |
| size.y = default_size; |
| const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); |
| ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); |
| if (!ItemAdd(bb, id)) |
| return false; |
| |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held); |
| |
| if (flags & ImGuiColorEditFlags_NoAlpha) |
| flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); |
| |
| ImVec4 col_without_alpha(col.x, col.y, col.z, 1.0f); |
| float grid_step = ImMin(size.x, size.y) / 2.99f; |
| float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); |
| ImRect bb_inner = bb; |
| float 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. |
| bb_inner.Expand(off); |
| if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col.w < 1.0f) |
| { |
| float mid_x = (float)(int)((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f); |
| RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight| ImDrawCornerFlags_BotRight); |
| window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft); |
| } |
| else |
| { |
| // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha |
| ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col : col_without_alpha; |
| if (col_source.w < 1.0f) |
| RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); |
| else |
| window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All); |
| } |
| RenderNavHighlight(bb, id); |
| if (g.Style.FrameBorderSize > 0.0f) |
| RenderFrameBorder(bb.Min, bb.Max, rounding); |
| else |
| window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border |
| |
| // Drag and Drop Source |
| // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test. |
| if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource()) |
| { |
| if (flags & ImGuiColorEditFlags_NoAlpha) |
| SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col, sizeof(float) * 3, ImGuiCond_Once); |
| else |
| SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col, sizeof(float) * 4, ImGuiCond_Once); |
| ColorButton(desc_id, col, flags); |
| SameLine(); |
| TextUnformatted("Color"); |
| EndDragDropSource(); |
| } |
| |
| // Tooltip |
| if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) |
| ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); |
| |
| if (pressed) |
| MarkItemEdited(id); |
| |
| return pressed; |
| } |
| |
| void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) |
| { |
| ImGuiContext& g = *GImGui; |
| if ((flags & ImGuiColorEditFlags__InputsMask) == 0) |
| flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputsMask; |
| if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0) |
| flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask; |
| if ((flags & ImGuiColorEditFlags__PickerMask) == 0) |
| flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask; |
| IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__InputsMask))); // Check only 1 option is selected |
| IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__DataTypeMask))); // Check only 1 option is selected |
| IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check only 1 option is selected |
| g.ColorEditOptions = flags; |
| } |
| |
| // Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. |
| void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) |
| { |
| ImGuiContext& g = *GImGui; |
| |
| 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]); |
| BeginTooltipEx(0, true); |
| |
| const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; |
| if (text_end > text) |
| { |
| TextUnformatted(text, text_end); |
| Separator(); |
| } |
| |
| ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); |
| ColorButton("##preview", ImVec4(col[0], col[1], col[2], col[3]), (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); |
| SameLine(); |
| if (flags & ImGuiColorEditFlags_NoAlpha) |
| 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]); |
| else |
| 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]); |
| EndTooltip(); |
| } |
| |
| void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) |
| { |
| bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__InputsMask); |
| bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask); |
| if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) |
| return; |
| ImGuiContext& g = *GImGui; |
| ImGuiColorEditFlags opts = g.ColorEditOptions; |
| if (allow_opt_inputs) |
| { |
| if (RadioButton("RGB", (opts & ImGuiColorEditFlags_RGB) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_RGB; |
| if (RadioButton("HSV", (opts & ImGuiColorEditFlags_HSV) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HSV; |
| if (RadioButton("HEX", (opts & ImGuiColorEditFlags_HEX) != 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HEX; |
| } |
| if (allow_opt_datatype) |
| { |
| if (allow_opt_inputs) Separator(); |
| if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8; |
| if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float; |
| } |
| |
| if (allow_opt_inputs || allow_opt_datatype) |
| Separator(); |
| if (Button("Copy as..", ImVec2(-1,0))) |
| OpenPopup("Copy"); |
| if (BeginPopup("Copy")) |
| { |
| 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]); |
| char buf[64]; |
| ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); |
| if (Selectable(buf)) |
| SetClipboardText(buf); |
| ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); |
| if (Selectable(buf)) |
| SetClipboardText(buf); |
| if (flags & ImGuiColorEditFlags_NoAlpha) |
| ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb); |
| else |
| ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca); |
| if (Selectable(buf)) |
| SetClipboardText(buf); |
| EndPopup(); |
| } |
| |
| g.ColorEditOptions = opts; |
| EndPopup(); |
| } |
| |
| void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) |
| { |
| bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask); |
| bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); |
| if ((!allow_opt_picker && !allow_opt_alpha_bar) || !ImGui::BeginPopup("context")) |
| return; |
| ImGuiContext& g = *GImGui; |
| if (allow_opt_picker) |
| { |
| ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function |
| ImGui::PushItemWidth(picker_size.x); |
| for (int picker_type = 0; picker_type < 2; picker_type++) |
| { |
| // Draw small/thumbnail version of each picker type (over an invisible button for selection) |
| if (picker_type > 0) ImGui::Separator(); |
| ImGui::PushID(picker_type); |
| ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha); |
| if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; |
| if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; |
| ImVec2 backup_pos = ImGui::GetCursorScreenPos(); |
| if (ImGui::Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup |
| g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask); |
| ImGui::SetCursorScreenPos(backup_pos); |
| ImVec4 dummy_ref_col; |
| memcpy(&dummy_ref_col.x, ref_col, sizeof(float) * (picker_flags & ImGuiColorEditFlags_NoAlpha ? 3 : 4)); |
| ImGui::ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags); |
| ImGui::PopID(); |
| } |
| ImGui::PopItemWidth(); |
| } |
| if (allow_opt_alpha_bar) |
| { |
| if (allow_opt_picker) ImGui::Separator(); |
| ImGui::CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); |
| } |
| ImGui::EndPopup(); |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Trees |
| // - TreeNode() |
| // - TreeNodeV() |
| // - TreeNodeEx() |
| // - TreeNodeExV() |
| // - TreeNodeBehavior() [Internal] |
| // - TreePush() |
| // - TreePop() |
| // - TreeAdvanceToLabelPos() |
| // - GetTreeNodeToLabelSpacing() |
| // - SetNextTreeNodeOpen() |
| // - CollapsingHeader() |
| //------------------------------------------------------------------------- |
| |
| bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| bool is_open = TreeNodeExV(str_id, 0, fmt, args); |
| va_end(args); |
| return is_open; |
| } |
| |
| bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); |
| va_end(args); |
| return is_open; |
| } |
| |
| bool ImGui::TreeNode(const char* label) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| return TreeNodeBehavior(window->GetID(label), 0, label, NULL); |
| } |
| |
| bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) |
| { |
| return TreeNodeExV(str_id, 0, fmt, args); |
| } |
| |
| bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) |
| { |
| return TreeNodeExV(ptr_id, 0, fmt, args); |
| } |
| |
| bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| return TreeNodeBehavior(window->GetID(label), flags, label, NULL); |
| } |
| |
| bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| bool is_open = TreeNodeExV(str_id, flags, fmt, args); |
| va_end(args); |
| return is_open; |
| } |
| |
| bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) |
| { |
| va_list args; |
| va_start(args, fmt); |
| bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); |
| va_end(args); |
| return is_open; |
| } |
| |
| bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); |
| return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end); |
| } |
| |
| bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); |
| return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end); |
| } |
| |
| bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) |
| { |
| if (flags & ImGuiTreeNodeFlags_Leaf) |
| return true; |
| |
| // We only write to the tree storage if the user clicks (or explicitly use SetNextTreeNode*** functions) |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = g.CurrentWindow; |
| ImGuiStorage* storage = window->DC.StateStorage; |
| |
| bool is_open; |
| if (g.NextTreeNodeOpenCond != 0) |
| { |
| if (g.NextTreeNodeOpenCond & ImGuiCond_Always) |
| { |
| is_open = g.NextTreeNodeOpenVal; |
| storage->SetInt(id, is_open); |
| } |
| else |
| { |
| // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. |
| const int stored_value = storage->GetInt(id, -1); |
| if (stored_value == -1) |
| { |
| is_open = g.NextTreeNodeOpenVal; |
| storage->SetInt(id, is_open); |
| } |
| else |
| { |
| is_open = stored_value != 0; |
| } |
| } |
| g.NextTreeNodeOpenCond = 0; |
| } |
| else |
| { |
| is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; |
| } |
| |
| // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). |
| // NB- If we are above max depth we still allow manually opened nodes to be logged. |
| if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && window->DC.TreeDepth < g.LogAutoExpandMaxDepth) |
| is_open = true; |
| |
| return is_open; |
| } |
| |
| bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; |
| const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f); |
| |
| if (!label_end) |
| label_end = FindRenderedTextEnd(label); |
| const ImVec2 label_size = CalcTextSize(label, label_end, false); |
| |
| // We vertically grow up to current line height up the typical widget height. |
| const float text_base_offset_y = ImMax(padding.y, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it |
| const float frame_height = ImMax(ImMin(window->DC.CurrentLineSize.y, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2); |
| ImRect frame_bb = ImRect(window->DC.CursorPos, ImVec2(window->Pos.x + GetContentRegionMax().x, window->DC.CursorPos.y + frame_height)); |
| if (display_frame) |
| { |
| // Framed header expand a little outside the default padding |
| frame_bb.Min.x -= (float)(int)(window->WindowPadding.x*0.5f) - 1; |
| frame_bb.Max.x += (float)(int)(window->WindowPadding.x*0.5f) - 1; |
| } |
| |
| const float text_offset_x = (g.FontSize + (display_frame ? padding.x*3 : padding.x*2)); // Collapser arrow width + Spacing |
| const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser |
| ItemSize(ImVec2(text_width, frame_height), text_base_offset_y); |
| |
| // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing |
| // (Ideally we'd want to add a flag for the user to specify if we want the hit test to be done up to the right side of the content or not) |
| const ImRect interact_bb = display_frame ? frame_bb : ImRect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + text_width + style.ItemSpacing.x*2, frame_bb.Max.y); |
| bool is_open = TreeNodeBehaviorIsOpen(id, flags); |
| |
| // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. |
| // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). |
| // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. |
| if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) |
| window->DC.TreeDepthMayJumpToParentOnPop |= (1 << window->DC.TreeDepth); |
| |
| bool item_add = ItemAdd(interact_bb, id); |
| window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; |
| window->DC.LastItemDisplayRect = frame_bb; |
| |
| if (!item_add) |
| { |
| if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) |
| TreePushRawID(id); |
| return is_open; |
| } |
| |
| // Flags that affects opening behavior: |
| // - 0(default) ..................... single-click anywhere to open |
| // - OpenOnDoubleClick .............. double-click anywhere to open |
| // - OpenOnArrow .................... single-click on arrow to open |
| // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open |
| ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0); |
| if (!(flags & ImGuiTreeNodeFlags_Leaf)) |
| button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; |
| if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) |
| button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); |
| |
| bool hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); |
| if (!(flags & ImGuiTreeNodeFlags_Leaf)) |
| { |
| bool toggled = false; |
| if (pressed) |
| { |
| toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); |
| if (flags & ImGuiTreeNodeFlags_OpenOnArrow) |
| toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); |
| if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) |
| toggled |= g.IO.MouseDoubleClicked[0]; |
| if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. |
| toggled = false; |
| } |
| |
| if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open) |
| { |
| toggled = true; |
| NavMoveRequestCancel(); |
| } |
| if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? |
| { |
| toggled = true; |
| NavMoveRequestCancel(); |
| } |
| |
| if (toggled) |
| { |
| is_open = !is_open; |
| window->DC.StateStorage->SetInt(id, is_open); |
| } |
| } |
| if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) |
| SetItemAllowOverlap(); |
| |
| // Render |
| const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); |
| const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); |
| if (display_frame) |
| { |
| // Framed type |
| RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding); |
| RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); |
| RenderArrow(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); |
| if (g.LogEnabled) |
| { |
| // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. |
| const char log_prefix[] = "\n##"; |
| const char log_suffix[] = "##"; |
| LogRenderedText(&text_pos, log_prefix, log_prefix+3); |
| RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); |
| LogRenderedText(&text_pos, log_suffix+1, log_suffix+3); |
| } |
| else |
| { |
| RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); |
| } |
| } |
| else |
| { |
| // Unframed typed for tree nodes |
| if (hovered || (flags & ImGuiTreeNodeFlags_Selected)) |
| { |
| RenderFrame(frame_bb.Min, frame_bb.Max, col, false); |
| RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); |
| } |
| |
| if (flags & ImGuiTreeNodeFlags_Bullet) |
| RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y)); |
| else if (!(flags & ImGuiTreeNodeFlags_Leaf)) |
| RenderArrow(frame_bb.Min + ImVec2(padding.x, g.FontSize*0.15f + text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); |
| if (g.LogEnabled) |
| LogRenderedText(&text_pos, ">"); |
| RenderText(text_pos, label, label_end, false); |
| } |
| |
| if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) |
| TreePushRawID(id); |
| return is_open; |
| } |
| |
| void ImGui::TreePush(const char* str_id) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| Indent(); |
| window->DC.TreeDepth++; |
| PushID(str_id ? str_id : "#TreePush"); |
| } |
| |
| void ImGui::TreePush(const void* ptr_id) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| Indent(); |
| window->DC.TreeDepth++; |
| PushID(ptr_id ? ptr_id : (const void*)"#TreePush"); |
| } |
| |
| void ImGui::TreePushRawID(ImGuiID id) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| Indent(); |
| window->DC.TreeDepth++; |
| window->IDStack.push_back(id); |
| } |
| |
| void ImGui::TreePop() |
| { |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = g.CurrentWindow; |
| Unindent(); |
| |
| window->DC.TreeDepth--; |
| if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) |
| if (g.NavIdIsAlive && (window->DC.TreeDepthMayJumpToParentOnPop & (1 << window->DC.TreeDepth))) |
| { |
| SetNavID(window->IDStack.back(), g.NavLayer); |
| NavMoveRequestCancel(); |
| } |
| window->DC.TreeDepthMayJumpToParentOnPop &= (1 << window->DC.TreeDepth) - 1; |
| |
| 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. |
| PopID(); |
| } |
| |
| void ImGui::TreeAdvanceToLabelPos() |
| { |
| ImGuiContext& g = *GImGui; |
| g.CurrentWindow->DC.CursorPos.x += GetTreeNodeToLabelSpacing(); |
| } |
| |
| // Horizontal distance preceding label when using TreeNode() or Bullet() |
| float ImGui::GetTreeNodeToLabelSpacing() |
| { |
| ImGuiContext& g = *GImGui; |
| return g.FontSize + (g.Style.FramePadding.x * 2.0f); |
| } |
| |
| void ImGui::SetNextTreeNodeOpen(bool is_open, ImGuiCond cond) |
| { |
| ImGuiContext& g = *GImGui; |
| if (g.CurrentWindow->SkipItems) |
| return; |
| g.NextTreeNodeOpenVal = is_open; |
| g.NextTreeNodeOpenCond = cond ? cond : ImGuiCond_Always; |
| } |
| |
| // CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). |
| // 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(). |
| bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader, label); |
| } |
| |
| bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| if (p_open && !*p_open) |
| return false; |
| |
| ImGuiID id = window->GetID(label); |
| bool is_open = TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap : 0), label); |
| if (p_open) |
| { |
| // Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. |
| ImGuiContext& g = *GImGui; |
| ImGuiItemHoveredDataBackup last_item_backup; |
| float button_radius = g.FontSize * 0.5f; |
| ImVec2 button_center = ImVec2(ImMin(window->DC.LastItemRect.Max.x, window->ClipRect.Max.x) - g.Style.FramePadding.x - button_radius, window->DC.LastItemRect.GetCenter().y); |
| if (CloseButton(window->GetID((void*)(intptr_t)(id+1)), button_center, button_radius)) |
| *p_open = false; |
| last_item_backup.Restore(); |
| } |
| |
| return is_open; |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Selectables |
| // - Selectable() |
| //------------------------------------------------------------------------- |
| |
| // Tip: pass an empty label (e.g. "##dummy") then you can use the space to draw other text or image. |
| // But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. |
| bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| |
| if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) // FIXME-OPT: Avoid if vertically clipped. |
| PopClipRect(); |
| |
| ImGuiID id = window->GetID(label); |
| ImVec2 label_size = CalcTextSize(label, NULL, true); |
| ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); |
| ImVec2 pos = window->DC.CursorPos; |
| pos.y += window->DC.CurrentLineTextBaseOffset; |
| ImRect bb_inner(pos, pos + size); |
| ItemSize(bb_inner); |
| |
| // Fill horizontal space. |
| ImVec2 window_padding = window->WindowPadding; |
| float max_x = (flags & ImGuiSelectableFlags_SpanAllColumns) ? GetWindowContentRegionMax().x : GetContentRegionMax().x; |
| float w_draw = ImMax(label_size.x, window->Pos.x + max_x - window_padding.x - window->DC.CursorPos.x); |
| ImVec2 size_draw((size_arg.x != 0 && !(flags & ImGuiSelectableFlags_DrawFillAvailWidth)) ? size_arg.x : w_draw, size_arg.y != 0.0f ? size_arg.y : size.y); |
| ImRect bb(pos, pos + size_draw); |
| if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) |
| bb.Max.x += window_padding.x; |
| |
| // Selectables are tightly packed together, we extend the box to cover spacing between selectable. |
| float spacing_L = (float)(int)(style.ItemSpacing.x * 0.5f); |
| float spacing_U = (float)(int)(style.ItemSpacing.y * 0.5f); |
| float spacing_R = style.ItemSpacing.x - spacing_L; |
| float spacing_D = style.ItemSpacing.y - spacing_U; |
| bb.Min.x -= spacing_L; |
| bb.Min.y -= spacing_U; |
| bb.Max.x += spacing_R; |
| bb.Max.y += spacing_D; |
| if (!ItemAdd(bb, (flags & ImGuiSelectableFlags_Disabled) ? 0 : id)) |
| { |
| if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) |
| PushColumnClipRect(); |
| return false; |
| } |
| |
| // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries |
| ImGuiButtonFlags button_flags = 0; |
| if (flags & ImGuiSelectableFlags_NoHoldingActiveID) button_flags |= ImGuiButtonFlags_NoHoldingActiveID; |
| if (flags & ImGuiSelectableFlags_PressedOnClick) button_flags |= ImGuiButtonFlags_PressedOnClick; |
| if (flags & ImGuiSelectableFlags_PressedOnRelease) button_flags |= ImGuiButtonFlags_PressedOnRelease; |
| if (flags & ImGuiSelectableFlags_Disabled) button_flags |= ImGuiButtonFlags_Disabled; |
| if (flags & ImGuiSelectableFlags_AllowDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; |
| bool hovered, held; |
| bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); |
| if (flags & ImGuiSelectableFlags_Disabled) |
| selected = false; |
| |
| // Hovering selectable with mouse updates NavId accordingly so navigation can be resumed with gamepad/keyboard (this doesn't happen on most widgets) |
| if (pressed || hovered) |
| if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) |
| { |
| g.NavDisableHighlight = true; |
| SetNavID(id, window->DC.NavLayerCurrent); |
| } |
| if (pressed) |
| MarkItemEdited(id); |
| |
| // Render |
| if (hovered || selected) |
| { |
| const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); |
| RenderFrame(bb.Min, bb.Max, col, false, 0.0f); |
| RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); |
| } |
| |
| if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.ColumnsSet) |
| { |
| PushColumnClipRect(); |
| bb.Max.x -= (GetContentRegionMax().x - max_x); |
| } |
| |
| if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); |
| RenderTextClipped(bb_inner.Min, bb.Max, label, NULL, &label_size, ImVec2(0.0f,0.0f)); |
| if (flags & ImGuiSelectableFlags_Disabled) PopStyleColor(); |
| |
| // Automatically close popups |
| if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) |
| CloseCurrentPopup(); |
| return pressed; |
| } |
| |
| bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) |
| { |
| if (Selectable(label, *p_selected, flags, size_arg)) |
| { |
| *p_selected = !*p_selected; |
| return true; |
| } |
| return false; |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: List Box |
| // - ListBox() |
| // - ListBoxHeader() |
| // - ListBoxFooter() |
| //------------------------------------------------------------------------- |
| |
| // FIXME: Rename to BeginListBox() |
| // Helper to calculate the size of a listbox and display a label on the right. |
| // Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an empty label "##empty" |
| bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| const ImGuiStyle& style = GetStyle(); |
| const ImGuiID id = GetID(label); |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. |
| ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y); |
| ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); |
| ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); |
| ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); |
| window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy. |
| |
| BeginGroup(); |
| if (label_size.x > 0) |
| RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); |
| |
| BeginChildFrame(id, frame_bb.GetSize()); |
| return true; |
| } |
| |
| // FIXME: Rename to BeginListBox() |
| bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items) |
| { |
| // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. |
| // We don't add +0.40f if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size. |
| // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution. |
| if (height_in_items < 0) |
| height_in_items = ImMin(items_count, 7); |
| float height_in_items_f = height_in_items < items_count ? (height_in_items + 0.40f) : (height_in_items + 0.00f); |
| |
| // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild(). |
| ImVec2 size; |
| size.x = 0.0f; |
| size.y = GetTextLineHeightWithSpacing() * height_in_items_f + GetStyle().ItemSpacing.y; |
| return ListBoxHeader(label, size); |
| } |
| |
| // FIXME: Rename to EndListBox() |
| void ImGui::ListBoxFooter() |
| { |
| ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow; |
| const ImRect bb = parent_window->DC.LastItemRect; |
| const ImGuiStyle& style = GetStyle(); |
| |
| EndChildFrame(); |
| |
| // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect) |
| // We call SameLine() to restore DC.CurrentLine* data |
| SameLine(); |
| parent_window->DC.CursorPos = bb.Min; |
| ItemSize(bb, style.FramePadding.y); |
| EndGroup(); |
| } |
| |
| bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) |
| { |
| const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items); |
| return value_changed; |
| } |
| |
| bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) |
| { |
| if (!ListBoxHeader(label, items_count, height_in_items)) |
| return false; |
| |
| // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper. |
| ImGuiContext& g = *GImGui; |
| bool value_changed = false; |
| ImGuiListClipper clipper(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. |
| while (clipper.Step()) |
| for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) |
| { |
| const bool item_selected = (i == *current_item); |
| const char* item_text; |
| if (!items_getter(data, i, &item_text)) |
| item_text = "*Unknown item*"; |
| |
| PushID(i); |
| if (Selectable(item_text, item_selected)) |
| { |
| *current_item = i; |
| value_changed = true; |
| } |
| if (item_selected) |
| SetItemDefaultFocus(); |
| PopID(); |
| } |
| ListBoxFooter(); |
| if (value_changed) |
| MarkItemEdited(g.CurrentWindow->DC.LastItemId); |
| |
| return value_changed; |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Data Plotting |
| // - PlotEx() [Internal] |
| // - PlotLines() |
| // - PlotHistogram() |
| //------------------------------------------------------------------------- |
| |
| void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| |
| const ImVec2 label_size = CalcTextSize(label, NULL, true); |
| if (graph_size.x == 0.0f) |
| graph_size.x = CalcItemWidth(); |
| if (graph_size.y == 0.0f) |
| graph_size.y = label_size.y + (style.FramePadding.y * 2); |
| |
| const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(graph_size.x, graph_size.y)); |
| const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); |
| 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)); |
| ItemSize(total_bb, style.FramePadding.y); |
| if (!ItemAdd(total_bb, 0, &frame_bb)) |
| return; |
| const bool hovered = ItemHoverable(inner_bb, 0); |
| |
| // Determine scale from values if not specified |
| if (scale_min == FLT_MAX || scale_max == FLT_MAX) |
| { |
| float v_min = FLT_MAX; |
| float v_max = -FLT_MAX; |
| for (int i = 0; i < values_count; i++) |
| { |
| const float v = values_getter(data, i); |
| v_min = ImMin(v_min, v); |
| v_max = ImMax(v_max, v); |
| } |
| if (scale_min == FLT_MAX) |
| scale_min = v_min; |
| if (scale_max == FLT_MAX) |
| scale_max = v_max; |
| } |
| |
| RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); |
| |
| if (values_count > 0) |
| { |
| int res_w = ImMin((int)graph_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); |
| int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); |
| |
| // Tooltip on hover |
| int v_hovered = -1; |
| if (hovered) |
| { |
| const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); |
| const int v_idx = (int)(t * item_count); |
| IM_ASSERT(v_idx >= 0 && v_idx < values_count); |
| |
| const float v0 = values_getter(data, (v_idx + values_offset) % values_count); |
| const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); |
| if (plot_type == ImGuiPlotType_Lines) |
| SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx+1, v1); |
| else if (plot_type == ImGuiPlotType_Histogram) |
| SetTooltip("%d: %8.4g", v_idx, v0); |
| v_hovered = v_idx; |
| } |
| |
| const float t_step = 1.0f / (float)res_w; |
| const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); |
| |
| float v0 = values_getter(data, (0 + values_offset) % values_count); |
| float t0 = 0.0f; |
| ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle |
| float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (-scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands |
| |
| const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); |
| const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); |
| |
| for (int n = 0; n < res_w; n++) |
| { |
| const float t1 = t0 + t_step; |
| const int v1_idx = (int)(t0 * item_count + 0.5f); |
| IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); |
| const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); |
| const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); |
| |
| // 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. |
| ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); |
| ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); |
| if (plot_type == ImGuiPlotType_Lines) |
| { |
| window->DrawList->AddLine(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); |
| } |
| else if (plot_type == ImGuiPlotType_Histogram) |
| { |
| if (pos1.x >= pos0.x + 2.0f) |
| pos1.x -= 1.0f; |
| window->DrawList->AddRectFilled(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); |
| } |
| |
| t0 = t1; |
| tp0 = tp1; |
| } |
| } |
| |
| // Text overlay |
| if (overlay_text) |
| 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)); |
| |
| if (label_size.x > 0.0f) |
| RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); |
| } |
| |
| struct ImGuiPlotArrayGetterData |
| { |
| const float* Values; |
| int Stride; |
| |
| ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } |
| }; |
| |
| static float Plot_ArrayGetter(void* data, int idx) |
| { |
| ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; |
| const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); |
| return v; |
| } |
| |
| 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) |
| { |
| ImGuiPlotArrayGetterData data(values, stride); |
| PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); |
| } |
| |
| 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) |
| { |
| PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); |
| } |
| |
| 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) |
| { |
| ImGuiPlotArrayGetterData data(values, stride); |
| PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); |
| } |
| |
| 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) |
| { |
| PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Value() helpers |
| // - Value() |
| //------------------------------------------------------------------------- |
| |
| void ImGui::Value(const char* prefix, bool b) |
| { |
| Text("%s: %s", prefix, (b ? "true" : "false")); |
| } |
| |
| void ImGui::Value(const char* prefix, int v) |
| { |
| Text("%s: %d", prefix, v); |
| } |
| |
| void ImGui::Value(const char* prefix, unsigned int v) |
| { |
| Text("%s: %d", prefix, v); |
| } |
| |
| void ImGui::Value(const char* prefix, float v, const char* float_format) |
| { |
| if (float_format) |
| { |
| char fmt[64]; |
| ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); |
| Text(fmt, prefix, v); |
| } |
| else |
| { |
| Text("%s: %.3f", prefix, v); |
| } |
| } |
| |
| //------------------------------------------------------------------------- |
| // WIDGETS: Menus |
| // - ImGuiMenuColumns |
| // - BeginMainMenuBar() |
| // - EndMainMenuBar() |
| // - BeginMenuBar() |
| // - EndMenuBar() |
| // - BeginMenu() |
| // - EndMenu() |
| // - MenuItem() |
| //------------------------------------------------------------------------- |
| |
| // Helpers for internal use |
| ImGuiMenuColumns::ImGuiMenuColumns() |
| { |
| Count = 0; |
| Spacing = Width = NextWidth = 0.0f; |
| memset(Pos, 0, sizeof(Pos)); |
| memset(NextWidths, 0, sizeof(NextWidths)); |
| } |
| |
| void ImGuiMenuColumns::Update(int count, float spacing, bool clear) |
| { |
| IM_ASSERT(Count <= IM_ARRAYSIZE(Pos)); |
| Count = count; |
| Width = NextWidth = 0.0f; |
| Spacing = spacing; |
| if (clear) memset(NextWidths, 0, sizeof(NextWidths)); |
| for (int i = 0; i < Count; i++) |
| { |
| if (i > 0 && NextWidths[i] > 0.0f) |
| Width += Spacing; |
| Pos[i] = (float)(int)Width; |
| Width += NextWidths[i]; |
| NextWidths[i] = 0.0f; |
| } |
| } |
| |
| float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double |
| { |
| NextWidth = 0.0f; |
| NextWidths[0] = ImMax(NextWidths[0], w0); |
| NextWidths[1] = ImMax(NextWidths[1], w1); |
| NextWidths[2] = ImMax(NextWidths[2], w2); |
| for (int i = 0; i < 3; i++) |
| NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f); |
| return ImMax(Width, NextWidth); |
| } |
| |
| float ImGuiMenuColumns::CalcExtraSpace(float avail_w) |
| { |
| return ImMax(0.0f, avail_w - Width); |
| } |
| |
| // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. |
| bool ImGui::BeginMainMenuBar() |
| { |
| ImGuiContext& g = *GImGui; |
| g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); |
| SetNextWindowPos(ImVec2(0.0f, 0.0f)); |
| SetNextWindowSize(ImVec2(g.IO.DisplaySize.x, g.NextWindowData.MenuBarOffsetMinVal.y + g.FontBaseSize + g.Style.FramePadding.y)); |
| PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); |
| PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0,0)); |
| ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; |
| bool is_open = Begin("##MainMenuBar", NULL, window_flags) && BeginMenuBar(); |
| PopStyleVar(2); |
| g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); |
| if (!is_open) |
| { |
| End(); |
| return false; |
| } |
| return true; |
| } |
| |
| void ImGui::EndMainMenuBar() |
| { |
| EndMenuBar(); |
| |
| // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window |
| ImGuiContext& g = *GImGui; |
| if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0) |
| FocusFrontMostActiveWindowIgnoringOne(g.NavWindow); |
| |
| End(); |
| } |
| |
| bool ImGui::BeginMenuBar() |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| if (!(window->Flags & ImGuiWindowFlags_MenuBar)) |
| return false; |
| |
| IM_ASSERT(!window->DC.MenuBarAppending); |
| BeginGroup(); // Backup position on layer 0 |
| PushID("##menubar"); |
| |
| // 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. |
| // 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. |
| ImRect bar_rect = window->MenuBarRect(); |
| ImRect clip_rect(ImFloor(bar_rect.Min.x + 0.5f), ImFloor(bar_rect.Min.y + window->WindowBorderSize + 0.5f), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding) + 0.5f), ImFloor(bar_rect.Max.y + 0.5f)); |
| clip_rect.ClipWith(window->OuterRectClipped); |
| PushClipRect(clip_rect.Min, clip_rect.Max, false); |
| |
| window->DC.CursorPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); |
| window->DC.LayoutType = ImGuiLayoutType_Horizontal; |
| window->DC.NavLayerCurrent++; |
| window->DC.NavLayerCurrentMask <<= 1; |
| window->DC.MenuBarAppending = true; |
| AlignTextToFramePadding(); |
| return true; |
| } |
| |
| void ImGui::EndMenuBar() |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return; |
| ImGuiContext& g = *GImGui; |
| |
| // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. |
| if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) |
| { |
| ImGuiWindow* nav_earliest_child = g.NavWindow; |
| while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) |
| nav_earliest_child = nav_earliest_child->ParentWindow; |
| if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && g.NavMoveRequestForward == ImGuiNavForward_None) |
| { |
| // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. |
| // 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 the hassle/cost) |
| IM_ASSERT(window->DC.NavLayerActiveMaskNext & 0x02); // Sanity check |
| FocusWindow(window); |
| SetNavIDWithRectRel(window->NavLastIds[1], 1, window->NavRectRel[1]); |
| g.NavLayer = 1; |
| g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. |
| g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; |
| NavMoveRequestCancel(); |
| } |
| } |
| |
| IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); |
| IM_ASSERT(window->DC.MenuBarAppending); |
| PopClipRect(); |
| PopID(); |
| window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->MenuBarRect().Min.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. |
| window->DC.GroupStack.back().AdvanceCursor = false; |
| EndGroup(); // Restore position on layer 0 |
| window->DC.LayoutType = ImGuiLayoutType_Vertical; |
| window->DC.NavLayerCurrent--; |
| window->DC.NavLayerCurrentMask >>= 1; |
| window->DC.MenuBarAppending = false; |
| } |
| |
| bool ImGui::BeginMenu(const char* label, bool enabled) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| const ImGuiStyle& style = g.Style; |
| const ImGuiID id = window->GetID(label); |
| |
| ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| bool pressed; |
| bool menu_is_open = IsPopupOpen(id); |
| bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].OpenParentId == window->IDStack.back()); |
| ImGuiWindow* backed_nav_window = g.NavWindow; |
| if (menuset_is_open) |
| g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) |
| |
| // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu (using FindBestWindowPosForPopup). |
| ImVec2 popup_pos, pos = window->DC.CursorPos; |
| if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) |
| { |
| // Menu inside an horizontal menu bar |
| // Selectable extend their highlight by half ItemSpacing in each direction. |
| // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() |
| popup_pos = ImVec2(pos.x - window->WindowPadding.x, pos.y - style.FramePadding.y + window->MenuBarHeight()); |
| window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); |
| PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); |
| float w = label_size.x; |
| pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); |
| PopStyleVar(); |
| window->DC.CursorPos.x += (float)(int)(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(). |
| } |
| else |
| { |
| // Menu inside a menu |
| popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); |
| float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, (float)(int)(g.FontSize * 1.20f)); // Feedback to next frame |
| float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); |
| pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_DrawFillAvailWidth | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); |
| if (!enabled) PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); |
| RenderArrow(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), ImGuiDir_Right); |
| if (!enabled) PopStyleColor(); |
| } |
| |
| const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id); |
| if (menuset_is_open) |
| g.NavWindow = backed_nav_window; |
| |
| bool want_open = false, want_close = false; |
| if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) |
| { |
| // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. |
| bool moving_within_opened_triangle = false; |
| if (g.HoveredWindow == window && g.OpenPopupStack.Size > g.CurrentPopupStack.Size && g.OpenPopupStack[g.CurrentPopupStack.Size].ParentWindow == window && !(window->Flags & ImGuiWindowFlags_MenuBar)) |
| { |
| if (ImGuiWindow* next_window = g.OpenPopupStack[g.CurrentPopupStack.Size].Window) |
| { |
| ImRect next_window_rect = next_window->Rect(); |
| ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta; |
| ImVec2 tb = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR(); |
| ImVec2 tc = (window->Pos.x < next_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR(); |
| float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, 5.0f, 30.0f); // add a bit of extra slack. |
| ta.x += (window->Pos.x < next_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues |
| tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -100.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale? |
| tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +100.0f); |
| moving_within_opened_triangle = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); |
| //window->DrawList->PushClipRectFullScreen(); window->DrawList->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); window->DrawList->PopClipRect(); // Debug |
| } |
| } |
| |
| want_close = (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_within_opened_triangle); |
| want_open = (!menu_is_open && hovered && !moving_within_opened_triangle) || (!menu_is_open && hovered && pressed); |
| |
| if (g.NavActivateId == id) |
| { |
| want_close = menu_is_open; |
| want_open = !menu_is_open; |
| } |
| if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open |
| { |
| want_open = true; |
| NavMoveRequestCancel(); |
| } |
| } |
| else |
| { |
| // Menu bar |
| if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it |
| { |
| want_close = true; |
| want_open = menu_is_open = false; |
| } |
| else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others |
| { |
| want_open = true; |
| } |
| else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open |
| { |
| want_open = true; |
| NavMoveRequestCancel(); |
| } |
| } |
| |
| 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.. }' |
| want_close = true; |
| if (want_close && IsPopupOpen(id)) |
| ClosePopupToLevel(g.CurrentPopupStack.Size); |
| |
| if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.CurrentPopupStack.Size) |
| { |
| // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame. |
| OpenPopup(label); |
| return false; |
| } |
| |
| menu_is_open |= want_open; |
| if (want_open) |
| OpenPopup(label); |
| |
| if (menu_is_open) |
| { |
| // 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) |
| SetNextWindowPos(popup_pos, ImGuiCond_Always); |
| ImGuiWindowFlags flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; |
| if (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) |
| flags |= ImGuiWindowFlags_ChildWindow; |
| menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) |
| } |
| |
| return menu_is_open; |
| } |
| |
| void ImGui::EndMenu() |
| { |
| // Nav: When a left move request _within our child menu_ failed, close the menu. |
| // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. |
| // However, it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. |
| ImGuiContext& g = *GImGui; |
| ImGuiWindow* window = g.CurrentWindow; |
| if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) |
| { |
| ClosePopupToLevel(g.OpenPopupStack.Size - 1); |
| NavMoveRequestCancel(); |
| } |
| |
| EndPopup(); |
| } |
| |
| bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) |
| { |
| ImGuiWindow* window = GetCurrentWindow(); |
| if (window->SkipItems) |
| return false; |
| |
| ImGuiContext& g = *GImGui; |
| ImGuiStyle& style = g.Style; |
| ImVec2 pos = window->DC.CursorPos; |
| ImVec2 label_size = CalcTextSize(label, NULL, true); |
| |
| ImGuiSelectableFlags flags = ImGuiSelectableFlags_PressedOnRelease | (enabled ? 0 : ImGuiSelectableFlags_Disabled); |
| bool pressed; |
| if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) |
| { |
| // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful |
| // Note that in this situation we render neither the shortcut neither the selected tick mark |
| float w = label_size.x; |
| window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); |
| PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing * 2.0f); |
| pressed = Selectable(label, false, flags, ImVec2(w, 0.0f)); |
| PopStyleVar(); |
| window->DC.CursorPos.x += (float)(int)(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(). |
| } |
| else |
| { |
| ImVec2 shortcut_size = shortcut ? CalcTextSize(shortcut, NULL) : ImVec2(0.0f, 0.0f); |
| float w = window->MenuColumns.DeclColumns(label_size.x, shortcut_size.x, (float)(int)(g.FontSize * 1.20f)); // Feedback for next frame |
| float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); |
| pressed = Selectable(label, false, flags | ImGuiSelectableFlags_DrawFillAvailWidth, ImVec2(w, 0.0f)); |
| if (shortcut_size.x > 0.0f) |
| { |
| PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); |
| RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false); |
| PopStyleColor(); |
| } |
| if (selected) |
| RenderCheckMark(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled), g.FontSize * 0.866f); |
| } |
| return pressed; |
| } |
| |
| bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) |
| { |
| if (MenuItem(label, shortcut, p_selected ? *p_selected : false, enabled)) |
| { |
| if (p_selected) |
| *p_selected = !*p_selected; |
| return true; |
| } |
| return false; |
| } |
| |