#include "UIWidgets.hpp" #define IMGUI_DEFINE_MATH_OPERATORS #include #include #include #include #include "2s2h/ShipUtils.h" #include #include "2s2h/BenPort.h" namespace UIWidgets { // Automatically adds newlines to break up text longer than a specified number of characters // Manually included newlines will still be respected and reset the line length // If line is midword when it hits the limit, text should break at the last encountered space std::string WrappedText(const char* text, unsigned int charactersPerLine) { std::string newText(text); const size_t tipLength = newText.length(); int lastSpace = -1; int currentLineLength = 0; for (unsigned int currentCharacter = 0; currentCharacter < tipLength; currentCharacter++) { if (newText[currentCharacter] == '\n') { currentLineLength = 0; lastSpace = -1; continue; } else if (newText[currentCharacter] == ' ') { lastSpace = currentCharacter; } if ((currentLineLength >= charactersPerLine) && (lastSpace >= 0)) { newText[lastSpace] = '\n'; currentLineLength = currentCharacter - lastSpace - 1; lastSpace = -1; } currentLineLength++; } return newText; } std::string WrappedText(const std::string& text, unsigned int charactersPerLine) { return WrappedText(text.c_str(), charactersPerLine); } void PaddedSeparator(bool padTop, bool padBottom, float extraVerticalTopPadding, float extraVerticalBottomPadding) { if (padTop) { Spacer(extraVerticalTopPadding); } ImGui::Separator(); if (padBottom) { Spacer(extraVerticalBottomPadding); } } void Tooltip(const char* text) { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", WrappedText(text).c_str()); } } void PushStyleMenu(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_PopupBg, ColorValues.at(Colors::DarkGray)); ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::DarkGray)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 15.0f)); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 3.0f); } void PushStyleMenu(Colors color) { PushStyleMenu(ColorValues.at(color)); } void PopStyleMenu() { ImGui::PopStyleVar(2); ImGui::PopStyleColor(4); } bool BeginMenu(const char* label, Colors color) { bool dirty = false; PushStyleMenu(color); ImGui::SetNextWindowSizeConstraints(ImVec2(200.0f, 0.0f), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::BeginMenu(label)) { dirty = true; } PopStyleMenu(); return dirty; } void PushStyleMenuItem(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_HeaderHovered, color); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0f, 15.0f)); } void PushStyleMenuItem(Colors color) { PushStyleMenuItem(ColorValues.at(color)); } void PopStyleMenuItem() { ImGui::PopStyleVar(1); ImGui::PopStyleColor(1); } bool MenuItem(const char* label, const char* shortcut, Colors color) { bool dirty = false; PushStyleMenuItem(color); if (ImGui::MenuItem(label, shortcut)) { dirty = true; } PopStyleMenuItem(); return dirty; } void PushStyleButton(const ImVec4& color, const ImVec2 padding) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } void PushStyleButton(Colors color, ImVec2 padding) { PushStyleButton(ColorValues.at(color), padding); } void PopStyleButton() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(4); } void PushStyleInput(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } void PushStyleInput(Colors color) { PushStyleInput(ColorValues.at(color)); } void PopStyleInput() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(7); } void PushStyleHeader(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(color.x, color.y, color.z, 0.6f)); } void PushStyleHeader(Colors color) { PushStyleHeader(ColorValues.at(color)); } void PopStyleHeader() { ImGui::PopStyleColor(3); } bool Button(const char* label, const ButtonOptions& options) { ImGui::BeginDisabled(options.disabled); PushStyleButton(options.color, options.padding); bool dirty = ImGui::Button(label, options.size); PopStyleButton(); ImGui::EndDisabled(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } return dirty; } bool IconButton(const char* strId, const char* icon, const ButtonOptions& options) { ImVec2 buttonScreenPos = ImGui::GetCursorScreenPos(); bool clicked = Button(strId, options); ImGuiCol textColorIndex = options.disabled ? ImGuiCol_TextDisabled : ImGuiCol_Text; ImU32 textColor = ImGui::GetColorU32(textColorIndex); ImFont* font = ImGui::GetFont(); unsigned int codepoint = 0; ImTextCharFromUtf8(&codepoint, icon, NULL); const ImFontGlyph* glyph = font->FindGlyph((ImWchar)codepoint); if (glyph != NULL) { float glyphWidth = glyph->X1 - glyph->X0; float glyphHeight = glyph->Y1 - glyph->Y0; ImVec2 penPos = ImVec2(buttonScreenPos.x + (options.size.x - glyphWidth) * 0.5f - glyph->X0, buttonScreenPos.y + (options.size.y - glyphHeight) * 0.5f - glyph->Y0); font->RenderChar(ImGui::GetWindowDrawList(), ImGui::GetFontSize(), penPos, textColor, (ImWchar)codepoint); } else { ImVec2 iconSize = ImGui::CalcTextSize(icon); ImVec2 iconPos = ImVec2(buttonScreenPos.x + (options.size.x - iconSize.x) * 0.5f, buttonScreenPos.y + (options.size.y - iconSize.y) * 0.5f); ImGui::GetWindowDrawList()->AddText(iconPos, textColor, icon); } return clicked; } bool WindowButton(const char* label, const char* cvarName, std::shared_ptr windowPtr, const WindowButtonOptions& options) { ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); std::string buttonText = label; bool dirty = false; if (CVarGetInteger(cvarName, 0)) { buttonText = ICON_FA_WINDOW_CLOSE " " + buttonText; } else { buttonText = ICON_FA_EXTERNAL_LINK_SQUARE " " + buttonText; } if (Button(buttonText.c_str(), { { options.tooltip, options.disabled, options.disabledTooltip }, options.size, options.padding, options.color })) { windowPtr->ToggleVisibility(); dirty = true; } ImGui::PopStyleVar(); return dirty; } void PushStyleCheckbox(const ImVec4& color, ImVec2 padding) { ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f)); ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.0f, 1.0f, 1.0f, 0.7f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 5.0f); } void PushStyleCheckbox(Colors color, ImVec2 padding) { PushStyleCheckbox(ColorValues.at(color), padding); } void PopStyleCheckbox() { ImGui::PopStyleVar(3); ImGui::PopStyleColor(5); } void Spacer(float height) { ImGui::Dummy(ImVec2(0.0f, height)); } void Separator(bool padTop, bool padBottom, float extraVerticalTopPadding, float extraVerticalBottomPadding) { if (padTop) { Spacer(extraVerticalTopPadding); } ImGui::Separator(); if (padBottom) { Spacer(extraVerticalBottomPadding); } } // Internal state stored per layout instance struct CardLayoutState { std::vector columnWidths; std::vector columnHeights; std::vector columnXPositions; int currentCardColumn; float startY; int columnsPerRow; float spacing; bool autoItemWidth; ImGuiChildFlags childFlags; }; static CardLayoutState* gCurrentCardLayout = nullptr; void BeginCardLayout(const CardLayoutOptions& options) { CardLayoutState* state = new CardLayoutState(); float availWidth = ImGui::GetContentRegionAvail().x; int columnsPerRow = ImClamp(options.columnsPerRow, 1, options.columnsPerRow); if (options.minColumnWidth > 0.0f) { float denom = options.minColumnWidth + options.spacing; if (denom > 0.0f) { int widthLimitedColumns = static_cast(ImFloor((availWidth + options.spacing) / denom)); columnsPerRow = ImClamp(widthLimitedColumns, 1, options.columnsPerRow); } } columnsPerRow = ImMax(columnsPerRow, 1); float columnWidth = (availWidth - (options.spacing * (columnsPerRow - 1))) / static_cast(columnsPerRow); // Initialize columns state->columnWidths.resize(columnsPerRow, columnWidth); state->columnHeights.resize(columnsPerRow, 0.0f); state->columnXPositions.resize(columnsPerRow); // Calculate X positions for each column float currentX = ImGui::GetCursorPosX(); for (int i = 0; i < columnsPerRow; i++) { state->columnXPositions[i] = currentX + (i * (columnWidth + options.spacing)); } state->startY = ImGui::GetCursorPosY(); state->currentCardColumn = 0; state->columnsPerRow = columnsPerRow; state->spacing = options.spacing; state->autoItemWidth = options.autoItemWidth; state->childFlags = options.childFlags; gCurrentCardLayout = state; } void BeginCard(const char* id) { CardLayoutState* state = gCurrentCardLayout; if (!state) return; // Find shortest column int shortestCol = 0; float shortestHeight = state->columnHeights[0]; for (int i = 1; i < state->columnsPerRow; i++) { if (state->columnHeights[i] < shortestHeight) { shortestHeight = state->columnHeights[i]; shortestCol = i; } } state->currentCardColumn = shortestCol; // Position cursor at this column's current height ImGui::SetCursorPosX(state->columnXPositions[state->currentCardColumn]); ImGui::SetCursorPosY(state->startY + state->columnHeights[state->currentCardColumn]); ImGui::BeginChild(id, ImVec2(state->columnWidths[state->currentCardColumn], 0), state->childFlags); // Auto-push item width to fill card if (state->autoItemWidth) { ImGui::PushItemWidth(-FLT_MIN); } } void EndCard() { CardLayoutState* state = gCurrentCardLayout; if (!state) return; // Auto-pop item width if (state->autoItemWidth) { ImGui::PopItemWidth(); } ImGui::EndChild(); // Get the height of the card we just rendered ImVec2 itemSize = ImGui::GetItemRectSize(); // Update this column's height (add card height + spacing) state->columnHeights[state->currentCardColumn] += itemSize.y + state->spacing; } void EndCardLayout() { if (!gCurrentCardLayout) { return; } CardLayoutState* state = gCurrentCardLayout; float maxHeight = 0.0f; for (float height : state->columnHeights) { maxHeight = ImMax(maxHeight, height); } if (maxHeight > 0.0f) { maxHeight -= state->spacing; ImGui::SetCursorPosY(state->startY + maxHeight); ImGui::Dummy(ImVec2(0.0f, 0.0f)); } delete state; gCurrentCardLayout = nullptr; } void RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; // Hide anything after a '##' string const char* text_display_end; if (hide_text_after_hash) { text_display_end = ImGui::FindRenderedTextEnd(text, text_end); } else { if (!text_end) text_end = text + strlen(text); // FIXME-OPT text_display_end = text_end; } if (text != text_display_end) { window->DrawList->AddText(g.Font, g.FontSize, pos, ImGui::GetColorU32(ImGuiCol_Text), text, text_display_end); if (g.LogEnabled) ImGui::LogRenderedText(&pos, text, text_display_end); } } bool Checkbox(const char* _label, bool* value, const CheckboxOptions& options) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGui::BeginDisabled(options.disabled); bool above = options.labelPosition == LabelPosition::Above; bool lpFar = options.labelPosition == LabelPosition::Far; bool right = options.alignment == ComponentAlignment::Right; bool none = options.labelPosition == LabelPosition::None; std::string labelStr = (none ? "##" : ""); labelStr.append(_label); const char* label = labelStr.c_str(); PushStyleCheckbox(options.color, options.padding); ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); const float square_sz = ImGui::GetFrameHeight(); ImVec2 pos = window->DC.CursorPos; if (right) { float labelOffsetX = (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz); if (!lpFar) { pos.x += ImGui::GetContentRegionAvail().x - (label_size.x + labelOffsetX); } } float bbAboveX = lpFar ? ImGui::GetContentRegionAvail().x : (label_size.x + (above ? 0 : (style.ItemInnerSpacing.x * 2.0f) + square_sz)); float bbAboveY = label_size.y + (above ? square_sz : 0) + (style.FramePadding.y * 2.0f); const ImRect total_bb(pos, pos + ImVec2(bbAboveX, bbAboveY)); ImGui::ItemSize(total_bb, style.FramePadding.y); if (!ImGui::ItemAdd(total_bb, id)) { PopStyleCheckbox(); ImGui::EndDisabled(); return false; } bool hovered, held, pressed; pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); if (pressed) { *value = !(*value); ImGui::MarkItemEdited(id); } ImVec2 checkPos = pos; ImVec2 labelPos = pos; if (options.labelPosition == LabelPosition::Above) { checkPos.y += label_size.y + (style.ItemInnerSpacing.y * 2.0f); } else { // Center with checkbox automatically labelPos.y += ImGui::GetStyle().FramePadding.y; } if (options.alignment == ComponentAlignment::Right) { checkPos.x = total_bb.Max.x - square_sz; } else { float labelFarOffset = ImGui::GetContentRegionAvail().x - label_size.x; float labelOffsetX = above ? 0 : (lpFar ? labelFarOffset : (style.ItemInnerSpacing.x * 2.0f) + square_sz); labelPos.x += labelOffsetX; } const ImRect check_bb(checkPos, checkPos + ImVec2(square_sz, square_sz)); ImGui::RenderNavHighlight(total_bb, id); ImGui::RenderFrame(check_bb.Min, check_bb.Max, ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); ImU32 check_col = ImGui::GetColorU32(ImGuiCol_CheckMark); bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; if (mixed_value) { // Undocumented tristate/mixed/indeterminate checkbox (#2644) // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all // widgets (not just checkbox) ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); } else if (*value) { const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); ImGui::RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); } RenderText(labelPos, label, ImGui::FindRenderedTextEnd(label), true); PopStyleCheckbox(); ImGui::EndDisabled(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } return pressed; } bool CVarCheckbox(const char* label, const char* cvarName, const CheckboxOptions& options) { bool dirty = false; bool value = (bool)CVarGetInteger(cvarName, options.defaultValue); if (Checkbox(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } bool StateButton(const char* str_id, const char* label, ImVec2 size, ButtonOptions options, ImGuiButtonFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) { return false; } const ImGuiStyle& style = g.Style; const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); const ImGuiID id = window->GetID(str_id); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); const float default_size = ImGui::GetFrameHeight(); ImGui::ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); if (!ImGui::ItemAdd(bb, id)) return false; if (g.LastItemData.ItemFlags & ImGuiItemFlags_ButtonRepeat) { ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); } bool hovered, held; bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, flags); if (g.LastItemData.ItemFlags & ImGuiItemFlags_ButtonRepeat) { ImGui::PopItemFlag(); // ImGuiItemFlags_ButtonRepeat; } PushStyleButton(options.color); // Render const ImU32 bg_col = ImGui::GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); // const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text); ImGui::RenderNavHighlight(bb, id); ImGui::RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); ImGui::RenderTextClipped(bb.Min + (style.FramePadding * 0.35f), bb.Max - (style.FramePadding / 4), label, NULL, &label_size, style.ButtonTextAlign, &bb); PopStyleButton(); /*ImGui::RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir);*/ IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); return pressed; } float CalcComboWidth(const char* preview_value, ImGuiComboFlags flags) { ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together if (flags & ImGuiComboFlags_WidthFitPreview) IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : ImGui::GetFrameHeight(); const float preview_width = ImGui::CalcTextSize(preview_value, NULL, true).x; float w = arrow_size + preview_width + (style.FramePadding.x * 2.0f); return w; } void PushStyleCombobox(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); } void PushStyleCombobox(Colors color) { PushStyleCombobox(ColorValues.at(color)); } void PopStyleCombobox() { ImGui::PopStyleVar(4); ImGui::PopStyleColor(9); } void PushStyleTabs(const ImVec4& color) { ImGui::PushStyleColor(ImGuiCol_Tab, ImVec4(color.x, color.y, color.z, 0.8f)); ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_TabActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(color.x, color.y, color.z, 0.5f)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(color.x, color.y, color.z, 0.6f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); } void PushStyleTabs(Colors color) { PushStyleTabs(ColorValues.at(color)); } void PopStyleTabs() { ImGui::PopStyleColor(6); ImGui::PopStyleVar(4); } void PushStyleSlider(Colors color_) { const ImVec4& color = ColorValues.at(color_); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(color.x, color.y, color.z, 1.0f)); ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(1.0, 1.0, 1.0, 0.4f)); ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, ImVec4(1.0, 1.0, 1.0, 0.5f)); ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); } void PopStyleSlider() { ImGui::PopStyleVar(4); ImGui::PopStyleColor(6); } bool SliderInt(const char* label, int32_t* value, const IntSliderOptions& options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); const char* invisibleLabel = invisibleLabelStr.c_str(); ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleSlider(options.color); float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { width = width - (ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x); } ImGui::AlignTextToFramePadding(); if (options.alignment == ComponentAlignment::Right) { ImGui::Text(label, *value); if (options.labelPosition == LabelPosition::Above) { ImGui::NewLine(); ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); } else if (options.labelPosition == LabelPosition::Near) { ImGui::SameLine(); } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); } } else if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Above) { ImGui::Text(label, *value); } } float buttonsWidth = 0; if (options.showResetButton) { buttonsWidth = ImGui::CalcTextSize(ICON_FA_UNDO).x + (ImGui::GetStyle().FramePadding.x * 2) + 3; } if (options.showAdjustmentButtons) { if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value > options.min) { *value -= options.step; if (options.clamp) { if (*value < options.min) { *value = options.min; } } dirty = true; } ImGui::SameLine(0, 3.0f); buttonsWidth += (ImGui::CalcTextSize("+").x + (ImGui::GetStyle().FramePadding.x * 2) + 3) * 2; } ImGui::SetNextItemWidth(width - buttonsWidth); if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_S32, value, &options.min, &options.max, options.format, options.flags)) { if (options.clamp) { if (*value < options.min) { *value = options.min; } if (*value > options.max) *value = options.max; } dirty = true; } if (options.showAdjustmentButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::CalcTextSize("+").x); if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value < options.max) { *value += options.step; if (options.clamp) { if (*value > options.max) *value = options.max; } dirty = true; } } if (options.showResetButton) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::CalcTextSize(ICON_FA_UNDO).x); if (Button(ICON_FA_UNDO, ButtonOptions{ .color = options.color }.Size(Sizes::Inline))) { *value = options.defaultValue; dirty = true; } } if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Near) { ImGui::SameLine(); ImGui::Text(label, *value); } else if (options.labelPosition == LabelPosition::Far) { ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x); ImGui::Text(label, *value); } } PopStyleSlider(); ImGui::EndDisabled(); ImGui::EndGroup(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s\n%s", WrappedText(options.tooltip).c_str(), "Edit (Ctrl + Left Click)"); } ImGui::PopID(); return dirty; } bool CVarSliderInt(const char* label, const char* cvarName, const IntSliderOptions& options) { bool dirty = false; int32_t value = CVarGetInteger(cvarName, options.defaultValue); if (SliderInt(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } void ClampFloat(float* value, float min, float max, float step) { int factor = 1; if (step < 1.0f) { factor *= 10; } if (step < 0.1f) { factor *= 10; } if (step < 0.01f) { factor *= 10; } if (step < 0.001f) { factor *= 10; } if (step < 0.0001f) { factor *= 10; } if (step < 0.00001f) { factor *= 10; } if (*value < min) { *value = min; } else if (*value > max) { *value = max; } else { int trunc = (int)std::round(*value * factor); *value = (float)trunc / factor; } } bool SliderFloat(const char* label, float* value, const FloatSliderOptions& options) { bool dirty = false; std::string invisibleLabelStr = "##" + std::string(label); const char* invisibleLabel = invisibleLabelStr.c_str(); float valueToDisplay = options.isPercentage ? *value * 100.0f : *value; float maxToDisplay = options.isPercentage ? options.max * 100.0f : options.max; float minToDisplay = options.isPercentage ? options.min * 100.0f : options.min; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleSlider(options.color); float labelSpacing = ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x; float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; if (options.labelPosition == LabelPosition::Near || options.labelPosition == LabelPosition::Far) { width = width - (ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x); } ImGui::AlignTextToFramePadding(); if (options.alignment == ComponentAlignment::Right) { ImGui::Text(label, valueToDisplay); if (options.labelPosition == LabelPosition::Above) { ImGui::NewLine(); ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); } else if (options.labelPosition == LabelPosition::Near) { width -= labelSpacing; ImGui::SameLine(); } else if (options.labelPosition == LabelPosition::Far || options.labelPosition == LabelPosition::None) { width -= labelSpacing; ImGui::SameLine(ImGui::GetContentRegionAvail().x - width); } } else if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Above) { ImGui::Text(label, valueToDisplay); } } float buttonsWidth = 0; if (options.showResetButton) { buttonsWidth = ImGui::CalcTextSize(ICON_FA_UNDO).x + (ImGui::GetStyle().FramePadding.x * 2) + 3; } if (options.showAdjustmentButtons) { if (Button("-", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value > options.min) { *value -= options.step; if (options.clamp) { ClampFloat(value, options.min, options.max, options.step); } dirty = true; } ImGui::SameLine(0, 3.0f); buttonsWidth += (ImGui::CalcTextSize("+").x + (ImGui::GetStyle().FramePadding.x * 2) + 3) * 2; } ImGui::SetNextItemWidth(width - buttonsWidth); if (ImGui::SliderScalar(invisibleLabel, ImGuiDataType_Float, &valueToDisplay, &minToDisplay, &maxToDisplay, options.format, options.flags)) { *value = options.isPercentage ? valueToDisplay / 100.0f : valueToDisplay; if (options.clamp) { ClampFloat(value, options.min, options.max, options.step); } dirty = true; } if (options.showAdjustmentButtons) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::CalcTextSize("+").x); if (Button("+", ButtonOptions{ .color = options.color }.Size(Sizes::Inline)) && *value < options.max) { *value += options.step; if (options.clamp) { ClampFloat(value, options.min, options.max, options.step); } dirty = true; } } if (options.showResetButton) { ImGui::SameLine(0, 3.0f); ImGui::SetNextItemWidth(ImGui::CalcTextSize(ICON_FA_UNDO).x); if (Button(ICON_FA_UNDO, ButtonOptions{ .color = options.color }.Size(Sizes::Inline))) { *value = options.defaultValue; dirty = true; } } if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Near) { ImGui::SameLine(); ImGui::Text(label, *value); } else if (options.labelPosition == LabelPosition::Far) { ImGui::SameLine(ImGui::GetContentRegionAvail().x - labelSpacing); ImGui::Text(label, *value); } } PopStyleSlider(); ImGui::EndDisabled(); ImGui::EndGroup(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s\n%s", WrappedText(options.tooltip).c_str(), "Edit (Ctrl + Left Click)"); } ImGui::PopID(); return dirty; } bool CVarSliderFloat(const char* label, const char* cvarName, const FloatSliderOptions& options) { bool dirty = false; float value = CVarGetFloat(cvarName, options.defaultValue); if (SliderFloat(label, &value, options)) { CVarSetFloat(cvarName, value); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } int InputTextResizeCallback(ImGuiInputTextCallbackData* data) { std::string* value = (std::string*)data->UserData; if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { value->resize(data->BufTextLen); data->Buf = (char*)value->c_str(); } return 0; } bool InputString(const char* label, std::string* value, const InputOptions& options) { bool dirty = false; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleInput(options.color); if (options.hasError) { ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red)); } float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Above) { ImGui::Text(label, *value->c_str()); } } else if (options.alignment == ComponentAlignment::Right) { if (options.labelPosition == LabelPosition::Above) { ImGui::NewLine(); ImGui::SameLine(width - ImGui::CalcTextSize(label).x); ImGui::Text(label, *value->c_str()); } } ImGui::SetNextItemWidth(width); ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize; if (options.secret) { flags |= ImGuiInputTextFlags_Password; } flags |= options.addedFlags; if (ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, flags, InputTextResizeCallback, value)) { dirty = true; } if (value->empty() && !options.placeholder.empty()) { ImGui::SameLine(17.0f); ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "%s", options.placeholder.c_str()); } if (options.hasError) { ImGui::PopStyleColor(); } PopStyleInput(); ImGui::EndDisabled(); ImGui::EndGroup(); if (options.hasError && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.errorText)) { ImGui::SetTooltip("%s", WrappedText(options.errorText).c_str()); } else if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } bool CVarInputString(const char* label, const char* cvarName, const InputOptions& options) { bool dirty = false; std::string value = CVarGetString(cvarName, options.defaultValue.c_str()); if (InputString(label, &value, options)) { CVarSetString(cvarName, value.c_str()); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } bool InputInt(const char* label, int32_t* value, const InputOptions& options) { bool dirty = false; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::BeginDisabled(options.disabled); PushStyleInput(options.color); float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; if (options.alignment == ComponentAlignment::Left) { if (options.labelPosition == LabelPosition::Above) { ImGui::Text(label, *value); } } else if (options.alignment == ComponentAlignment::Right) { if (options.labelPosition == LabelPosition::Above) { ImGui::NewLine(); ImGui::SameLine(width - ImGui::CalcTextSize(label).x); ImGui::Text(label, *value); } } ImGui::SetNextItemWidth(width); if (ImGui::InputScalar(label, ImGuiDataType_S32, value, nullptr, nullptr, nullptr, options.addedFlags)) { dirty = true; } if ((ImGui::GetItemStatusFlags() & ImGuiItemStatusFlags_Edited) && !options.placeholder.empty()) { ImGui::SameLine(17.0f); ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.4f), "%s", options.placeholder.c_str()); } PopStyleInput(); ImGui::EndDisabled(); ImGui::EndGroup(); if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.disabledTooltip)) { ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } ImGui::PopID(); return dirty; } bool CVarInputInt(const char* label, const char* cvarName, const InputOptions& options) { bool dirty = false; int32_t defaultValue = std::stoi(options.defaultValue); int32_t value = CVarGetInteger(cvarName, defaultValue); if (InputInt(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } bool CVarColorPicker(const char* label, const char* valueCvar, Color_RGBA8 defaultColor, bool hasAlpha, const char* lockedCvar, UIWidgets::Colors themeColor) { Color_RGBA8 color = CVarGetColor(valueCvar, defaultColor); ImVec4 colorVec = ImVec4(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f); bool changed = false; bool locked = lockedCvar != nullptr && CVarGetInteger(lockedCvar, 0); ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs; ImGui::BeginDisabled(locked); PushStyleCombobox(UIWidgets::Colors::DarkGray); if (hasAlpha) { changed = ImGui::ColorEdit4(label, (float*)&colorVec, flags | ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreview); } else { changed = ImGui::ColorEdit3(label, (float*)&colorVec, flags | ImGuiColorEditFlags_NoAlpha); } PopStyleCombobox(); ImGui::EndDisabled(); if (changed) { color.r = (uint8_t)(colorVec.x * 255.0f); color.g = (uint8_t)(colorVec.y * 255.0f); color.b = (uint8_t)(colorVec.z * 255.0f); color.a = (uint8_t)(colorVec.w * 255.0f); CVarSetColor(valueCvar, color); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(valueCvar); } return changed; } bool RadioButton(const char* label, bool active, const RadioButtonsOptions& options) { ImGuiWindow* window = ImGui::GetCurrentWindow(); if (window->SkipItems) return false; ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); const float square_sz = ImGui::GetFrameHeight(); const ImVec2 pos = window->DC.CursorPos; const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); const ImRect total_bb( pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); ImGui::ItemSize(total_bb, style.FramePadding.y); if (!ImGui::ItemAdd(total_bb, id)) return false; ImVec2 center = check_bb.GetCenter(); center.x = IM_ROUND(center.x); center.y = IM_ROUND(center.y); const float radius = (square_sz - 1.0f) * 0.5f; bool hovered, held; bool pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held); if (pressed) ImGui::MarkItemEdited(id); ImGui::RenderNavCursor(total_bb, id); const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); window->DrawList->AddCircleFilled(center, radius, ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); if (active) { const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); window->DrawList->AddCircleFilled(center, radius - pad, ImGui::GetColorU32(ImGuiCol_CheckMark)); } if (style.FrameBorderSize > 0.0f) { window->DrawList->AddCircle(center + ImVec2(1, 1), radius, ImGui::GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize); window->DrawList->AddCircle(center, radius, ImGui::GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize); } ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); if (g.LogEnabled) ImGui::LogRenderedText(&label_pos, active ? "(x)" : "( )"); if (label_size.x > 0.0f) RenderText(label_pos, label, ImGui::FindRenderedTextEnd(label), true); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } bool CVarRadioButton(const char* text, const char* cvarName, int32_t id, const RadioButtonsOptions& options) { std::string make_invisible = "##" + std::string(text) + std::string(cvarName); bool ret = false; int val = CVarGetInteger(cvarName, options.defaultIndex); PushStyleCheckbox(options.color); if (ImGui::RadioButton(make_invisible.c_str(), id == val)) { CVarSetInteger(cvarName, id); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ret = true; } ImGui::SameLine(); ImGui::Text("%s", text); PopStyleCheckbox(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !Ship_IsCStringEmpty(options.tooltip)) { ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); } return ret; } void DrawFlagArray32(const std::string& name, uint32_t& flags, Colors color) { ImGui::PushID(name.c_str()); for (int32_t flagIndex = 0; flagIndex < 32; flagIndex++) { if ((flagIndex % 8) != 0) { ImGui::SameLine(); } ImGui::PushID(flagIndex); uint32_t bitMask = 1 << flagIndex; bool flag = (flags & bitMask) != 0; PushStyleCheckbox(color); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 6.0f)); std::string id = fmt::format("##{}{}", name, flagIndex); if (ImGui::Checkbox(id.c_str(), &flag)) { if (flag) { flags |= bitMask; } else { flags &= ~bitMask; } } if (ImGui::IsItemHovered()) { std::string label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); ImGui::SetTooltip("%s", label.c_str()); } ImGui::PopStyleVar(); PopStyleCheckbox(); ImGui::PopID(); } ImGui::PopID(); } void DrawFlagArray16(const std::string& name, uint16_t& flags, Colors color) { ImGui::PushID(name.c_str()); for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { if ((flagIndex % 8) != 0) { ImGui::SameLine(); } ImGui::PushID(flagIndex); uint16_t bitMask = 1 << flagIndex; bool flag = (flags & bitMask) != 0; PushStyleCheckbox(color); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 6.0f)); std::string id = fmt::format("##{}{}", name, flagIndex); if (ImGui::Checkbox(id.c_str(), &flag)) { if (flag) { flags |= bitMask; } else { flags &= ~bitMask; } } if (ImGui::IsItemHovered()) { std::string label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); ImGui::SetTooltip("%s", label.c_str()); } ImGui::PopStyleVar(); PopStyleCheckbox(); ImGui::PopID(); } ImGui::PopID(); } void DrawFlagTableArray16(const FlagTable& flagTable, uint16_t& flags) { ImGui::PushID(flagTable.name); for (int16_t flagIndex = 0; flagIndex < 16; flagIndex++) { if ((flagIndex % 8) != 0) { ImGui::SameLine(); } ImGui::PushID(flagIndex); uint16_t bitMask = 1 << flagIndex; bool flag = (flags & bitMask) != 0; FlagEntry flagEntry = flagTable.entries.at(flagIndex); PushStyleCheckbox(LightBlue); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 6.0f)); std::string id = fmt::format("##{}{}", flagTable.name, flagIndex); if (ImGui::Checkbox(id.c_str(), &flag)) { if (flag) { flags |= bitMask; } else { flags &= ~bitMask; } } if (ImGui::IsItemHovered()) { std::string label = WrappedText(flagEntry.description, 60).c_str(); if (!label.size()) { label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); } ImGui::SetTooltip("%s", label.c_str()); } ImGui::PopStyleVar(); PopStyleCheckbox(); ImGui::PopID(); } ImGui::PopID(); } template static void DrawFlagTableArrayRowImpl(const FlagTable& flagTable, uint16_t row, T& flags) { constexpr int16_t width = sizeof(T) * 8; ImGui::PushID(flagTable.name); for (int16_t flagIndex = 0; flagIndex < width; flagIndex++) { size_t flagEntryIndex = row * width + flagIndex; if (flagEntryIndex >= flagTable.entries.size()) { break; } if (flagIndex != 0) { ImGui::SameLine(); } ImGui::PushID(flagIndex); T bitMask = 1 << flagIndex; bool flag = (flags & bitMask) != 0; FlagEntry flagEntry = flagTable.entries.at(flagEntryIndex); PushStyleCheckbox(LightBlue); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 6.0f)); std::string id = fmt::format("####{}", flagIndex); if (ImGui::Checkbox(id.c_str(), &flag)) { if (flag) { flags |= bitMask; } else { flags &= ~bitMask; } } if (ImGui::IsItemHovered()) { std::string label = WrappedText(flagEntry.description, 60).c_str(); if (!label.size()) { label = fmt::format("0x{:02X} ({})", flagIndex, flagIndex); } ImGui::SetTooltip("%s", label.c_str()); } ImGui::PopStyleVar(); PopStyleCheckbox(); ImGui::PopID(); } ImGui::PopID(); } void DrawFlagTableArray16(const FlagTable& flagTable, uint16_t row, uint16_t& flags) { DrawFlagTableArrayRowImpl(flagTable, row, flags); } void DrawFlagTableArray8(const FlagTable& flagTable, uint16_t row, uint8_t& flags) { DrawFlagTableArrayRowImpl(flagTable, row, flags); } void DrawFlagTableArray8Mask(const FlagTable& flagTable, uint16_t row, uint8_t& flags) { ImGui::PushID(flagTable.name); for (int8_t flagIndex = 0; flagIndex < 8; flagIndex++) { if ((flagIndex % 8) != 0) { ImGui::SameLine(); } ImGui::PushID(flagIndex); uint8_t bitMask = 1 << flagIndex; bool flag = (flags & bitMask) != 0; FlagEntry flagEntry = flagTable.entries.at(row * 8 + flagIndex); PushStyleCheckbox(flagEntry.type == PERSISTENT ? Gray : (flagEntry.type == CYCLE_RESET ? LightBlue : Orange)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0f, 6.0f)); std::string id = fmt::format("##{}{}", flagTable.name, flagIndex); if (ImGui::Checkbox(id.c_str(), &flag)) { if (flag) { flags |= bitMask; } else { flags &= ~bitMask; } } if (ImGui::IsItemHovered()) { std::string label = WrappedText(flagEntry.description, 60).c_str(); label += fmt::format("{}0x{:02X} ({})", label.size() ? "\n" : "", bitMask, flagIndex); ImGui::SetTooltip("%s", label.c_str()); } ImGui::PopStyleVar(); PopStyleCheckbox(); ImGui::PopID(); } ImGui::PopID(); } std::map buttonMap = { { "A", BTN_A }, { "B", BTN_B }, { "Z", BTN_Z }, { "START", BTN_START }, { "D-Up", BTN_DUP }, { "D-Down", BTN_DDOWN }, { "D-Left", BTN_DLEFT }, { "D-Right", BTN_DRIGHT }, { "L", BTN_L }, { "R", BTN_R }, { "C-Up", BTN_CUP }, { "C-Down", BTN_CDOWN }, { "C-Left", BTN_CLEFT }, { "C-Right", BTN_CRIGHT }, { "Modifier 1", BTN_CUSTOM_MODIFIER1 }, { "Modifier 2", BTN_CUSTOM_MODIFIER2 }, }; bool BtnSelector(const char* label, int32_t* value, const BtnSelectorOptions& options) { bool dirty = false; ImGui::PushID(label); ImGui::BeginGroup(); ImGui::AlignTextToFramePadding(); ImGui::Text("%s", label); ImGui::BeginDisabled(false); PushStyleCombobox(options.color); ImGui::BeginChild("ButtonCombo", ImVec2(0, ImGui::GetFrameHeightWithSpacing() + 14.0f), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); int32_t currentValue = *value; int index = 0; for (const auto& [buttonName, buttonMask] : buttonMap) { if (currentValue & buttonMask) { ImGui::PushID(buttonName.c_str()); if (index++ > 0) { ImGui::Text("+"); ImGui::SameLine(); } if (UIWidgets::Button(buttonName.c_str(), UIWidgets::ButtonOptions() .Tooltip("Remove this button from the combination") .Color(UIWidgets::Colors::Gray) .Size(UIWidgets::Sizes::Inline))) { currentValue &= ~buttonMask; dirty = true; } ImGui::PopID(); ImGui::SameLine(); } } if (UIWidgets::Button("+", UIWidgets::ButtonOptions({ { .tooltip = "Add a button to the combination" } }) .Size(UIWidgets::Sizes::Inline) .Color(options.color))) { ImGui::OpenPopup("Add Button"); } if (ImGui::BeginPopup("Add Button")) { UIWidgets::PushStyleMenuItem(); for (const auto& [buttonName, buttonMask] : buttonMap) { if (!(currentValue & buttonMask)) { if (ImGui::MenuItem(buttonName.c_str())) { currentValue |= buttonMask; dirty = true; } } } UIWidgets::PopStyleMenuItem(); ImGui::EndPopup(); } ImGui::SameLine(); if (UIWidgets::Button(ICON_FA_UNDO, UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline).Color(options.color))) { currentValue = options.defaultValue; dirty = true; } ImGui::EndChild(); PopStyleCombobox(); ImGui::EndDisabled(); ImGui::EndGroup(); ImGui::PopID(); if (dirty) { *value = currentValue; } return dirty; } bool CVarBtnSelector(const char* label, const char* cvarName, const BtnSelectorOptions& options) { bool dirty = false; int32_t value = CVarGetInteger(cvarName, options.defaultValue); if (BtnSelector(label, &value, options)) { CVarSetInteger(cvarName, value); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ShipInit::Init(cvarName); dirty = true; } return dirty; } } // namespace UIWidgets ImVec4 GetRandomValue() { #if !defined(__SWITCH__) && !defined(__WIIU__) std::random_device rd; std::mt19937 rng(rd()); #else size_t seed = std::hash{}(std::to_string(rand())); std::mt19937_64 rng(seed); #endif std::uniform_int_distribution dist(0, 255 - 1); ImVec4 NewColor; NewColor.x = (float)(dist(rng)) / 255.0f; NewColor.y = (float)(dist(rng)) / 255.0f; NewColor.z = (float)(dist(rng)) / 255.0f; return NewColor; } Color_RGBA8 RGBA8FromVec(ImVec4 vec) { Color_RGBA8 color = { vec.x * 255, vec.y * 255, vec.z * 255, vec.w * 255 }; return color; } ImVec4 VecFromRGBA8(Color_RGBA8 color) { ImVec4 vec = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f }; return vec; }