From 82ceb2e11fe0181c4e3be7afc8df857db5e71bb8 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Fri, 8 Aug 2025 21:25:52 +0200 Subject: [PATCH] impr: Better auto updater, added support for updating nightlies --- .../include/hex/api/content_registry.hpp | 3 + lib/libimhex/source/api/content_registry.cpp | 4 +- lib/libimhex/source/api/imhex_api.cpp | 2 +- main/updater/source/main.cpp | 166 ++++++++++------- plugins/builtin/source/content/init_tasks.cpp | 170 +++++++++++------- plugins/builtin/source/content/ui_items.cpp | 13 +- .../source/content/window_decoration.cpp | 6 +- 7 files changed, 220 insertions(+), 144 deletions(-) diff --git a/lib/libimhex/include/hex/api/content_registry.hpp b/lib/libimhex/include/hex/api/content_registry.hpp index afdd28870..3dd540812 100644 --- a/lib/libimhex/include/hex/api/content_registry.hpp +++ b/lib/libimhex/include/hex/api/content_registry.hpp @@ -821,6 +821,7 @@ EXPORT_MODULE namespace hex { struct TitleBarButton { std::string icon; + ImGuiCustomCol color; UnlocalizedString unlocalizedTooltip; ClickCallback callback; }; @@ -1007,11 +1008,13 @@ EXPORT_MODULE namespace hex { /** * @brief Adds a new title bar button * @param icon The icon to use for the button + * @param color The color of the icon * @param unlocalizedTooltip The unlocalized tooltip to use for the button * @param function The function to call when the button is clicked */ void addTitleBarButton( const std::string &icon, + ImGuiCustomCol color, const UnlocalizedString &unlocalizedTooltip, const impl::ClickCallback &function ); diff --git a/lib/libimhex/source/api/content_registry.cpp b/lib/libimhex/source/api/content_registry.cpp index 961ed8462..2410f5731 100644 --- a/lib/libimhex/source/api/content_registry.cpp +++ b/lib/libimhex/source/api/content_registry.cpp @@ -1071,8 +1071,8 @@ namespace hex { impl::s_sidebarItems->push_back({ icon, function, enabledCallback }); } - void addTitleBarButton(const std::string &icon, const UnlocalizedString &unlocalizedTooltip, const impl::ClickCallback &function) { - impl::s_titlebarButtons->push_back({ icon, unlocalizedTooltip, function }); + void addTitleBarButton(const std::string &icon, ImGuiCustomCol color, const UnlocalizedString &unlocalizedTooltip, const impl::ClickCallback &function) { + impl::s_titlebarButtons->push_back({ icon, color, unlocalizedTooltip, function }); } void addWelcomeScreenQuickSettingsToggle(const std::string &icon, const UnlocalizedString &unlocalizedTooltip, bool defaultState, const impl::ToggleCallback &function) { diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index 048d5f7be..8bef2903f 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -941,7 +941,7 @@ namespace hex { std::string updateTypeString; switch (updateType) { case UpdateType::Stable: - updateTypeString = "latest"; + updateTypeString = "stable"; break; case UpdateType::Nightly: updateTypeString = "nightly"; diff --git a/main/updater/source/main.cpp b/main/updater/source/main.cpp index 9322ebd36..a68100505 100644 --- a/main/updater/source/main.cpp +++ b/main/updater/source/main.cpp @@ -7,14 +7,11 @@ using namespace std::literals::string_literals; -std::string getUpdateUrl(std::string_view versionType, std::string_view operatingSystem) { +std::string getArtifactUrl(std::string_view artifactEnding, hex::ImHexApi::System::UpdateType updateType) { // Get the latest version info from the ImHex API const auto response = hex::HttpRequest("GET", - ImHexApiURL + fmt::format("/update/{}/{}", - versionType, - operatingSystem - ) - ).execute().get(); + GitHubApiURL + "/releases"s + ).execute().get(); const auto &data = response.getData(); @@ -25,7 +22,30 @@ std::string getUpdateUrl(std::string_view versionType, std::string_view operatin return { }; } - return data; + try { + const auto json = nlohmann::json::parse(data); + + for (const auto &release : json) { + if (updateType == hex::ImHexApi::System::UpdateType::Stable && !release["target_commitish"].get().starts_with("releases/v")) + continue; + if (updateType == hex::ImHexApi::System::UpdateType::Nightly && release["tag_name"].get() != "nightly") + continue; + + // Loop over all assets in the release + for (const auto &asset : release["assets"]) { + // Check if the asset name ends with the specified artifact ending + if (asset["name"].get().ends_with(artifactEnding)) { + return asset["browser_download_url"].get(); + } + } + } + + hex::log::error("No suitable artifact found for ending: {}", artifactEnding); + return { }; + } catch (const std::exception &e) { + hex::log::error("Failed to parse latest version info: {}", e.what()); + return { }; + } } std::optional downloadUpdate(const std::string &url) { @@ -40,20 +60,20 @@ std::optional downloadUpdate(const std::string &url) { const auto &data = response.getData(); + const auto updateFileName = wolv::util::splitString(url, "/").back(); + // Write the update to a file std::fs::path filePath; { - constexpr static auto UpdateFileName = "update.hexupd"; - // Loop over all available paths wolv::io::File file; for (const auto &path : hex::paths::Config.write()) { // Remove any existing update files - wolv::io::fs::remove(path / UpdateFileName); + wolv::io::fs::remove(path / updateFileName); // If a valid location hasn't been found already, try to create a new file if (!file.isValid()) - file = wolv::io::File(path / UpdateFileName, wolv::io::File::Mode::Create); + file = wolv::io::File(path / updateFileName, wolv::io::File::Mode::Create); } // If the update data can't be written to any of the default paths, the update cannot continue @@ -74,67 +94,64 @@ std::optional downloadUpdate(const std::string &url) { return filePath; } -std::string getUpdateType() { +#if defined(__x86_64__) + #define ARCH_DEPENDENT(x86_64, arm64) x86_64 +#elif defined(__arm64__) + #define ARCH_DEPENDENT(x86_64, arm64) arm64 +#else + #define ARCH_DEPENDENT(x86_64, arm64) "" +#endif + +std::string_view getUpdateArtifactEnding() { #if defined (OS_WINDOWS) - if (!hex::ImHexApi::System::isPortableVersion()) - return "win-msi"; + if (!hex::ImHexApi::System::isPortableVersion()) { + return ARCH_DEPENDENT("Windows-x86_64.msi", "Windows-arm64.msi"); + } #elif defined (OS_MACOS) - #if defined(__x86_64__) - return "macos-dmg-x86"; - #elif defined(__arm64__) - return "macos-dmg-arm"; - #endif + return ARCH_DEPENDENT("macOS-x86_64.dmg", "macOS-arm64.dmg"); #elif defined (OS_LINUX) if (hex::executeCommand("grep 'ID=ubuntu' /etc/os-release") == 0) { if (hex::executeCommand("grep 'VERSION_ID=\"24.04\"' /etc/os-release") == 0) - return "linux-deb-24.04"; + return ARCH_DEPENDENT("Ubuntu-24.04-x86_64.deb", ""); else if (hex::executeCommand("grep 'VERSION_ID=\"24.10\"' /etc/os-release") == 0) - return "linux-deb-24.10"; + return ARCH_DEPENDENT("Ubuntu-24.10-x86_64.deb", ""); + else if (hex::executeCommand("grep 'VERSION_ID=\"25.04\"' /etc/os-release") == 0) + return ARCH_DEPENDENT("Ubuntu-25.04-x86_64.deb", ""); } else if (hex::executeCommand("grep 'ID=fedora' /etc/os-release") == 0) { - if (hex::executeCommand("grep 'VERSION_ID=\"40\"' /etc/os-release") == 0) - return "linux-rpm-40"; - else if (hex::executeCommand("grep 'VERSION_ID=\"41\"' /etc/os-release") == 0) - return "linux-rpm-41"; + if (hex::executeCommand("grep 'VERSION_ID=\"41\"' /etc/os-release") == 0) + return ARCH_DEPENDENT("Fedora-41-x86_64.rpm", ""); + else if (hex::executeCommand("grep 'VERSION_ID=\"42\"' /etc/os-release") == 0) + return ARCH_DEPENDENT("Fedora-42-x86_64.rpm", ""); else if (hex::executeCommand("grep 'VERSION_ID=\"rawhide\"' /etc/os-release") == 0) - return "linux-rpm-rawhide"; + return ARCH_DEPENDENT("Fedora-rawhide-x86_64.rpm", ""); } else if (hex::executeCommand("grep '^NAME=\"Arch Linux\"' /etc/os-release") == 0) { - return "linux-arch"; + return ARCH_DEPENDENT("ArchLinux-x86_64.pkg.tar.zst", ""); } #endif return ""; } -int installUpdate(const std::string &type, std::fs::path updatePath) { +int installUpdate(const std::fs::path &updatePath) { struct UpdateHandler { - const char *type; - const char *extension; + const char *ending; const char *command; }; constexpr static auto UpdateHandlers = { - UpdateHandler { "win-msi", ".msi", "msiexec /i \"{}\" /qb" }, - UpdateHandler { "macos-dmg-x86", ".dmg", "hdiutil attach -autoopen \"{}\"" }, - UpdateHandler { "macos-dmg-arm", ".dmg", "hdiutil attach -autoopen \"{}\"" }, - UpdateHandler { "linux-deb-24.04", ".deb", "sudo apt update && sudo apt install -y --fix-broken \"{}\"" }, - UpdateHandler { "linux-deb-24.10", ".deb", "sudo apt update && sudo apt install -y --fix-broken \"{}\"" }, - UpdateHandler { "linux-rpm-40", ".rpm", "sudo rpm -i \"{}\"" }, - UpdateHandler { "linux-rpm-41", ".rpm", "sudo rpm -i \"{}\"" }, - UpdateHandler { "linux-rpm-rawhide", ".rpm", "sudo rpm -i \"{}\"" }, - UpdateHandler { "linux-arch", ".zst", "sudo pacman -Syy && sudo pacman -U --noconfirm \"{}\"" } + UpdateHandler { ".msi", "msiexec /i \"{}\" /qb" }, + UpdateHandler { ".dmg", "hdiutil attach -autoopen \"{}\"" }, + UpdateHandler { ".deb", "sudo apt update && sudo apt install -y --fix-broken \"{}\"" }, + UpdateHandler { ".rpm", "sudo rpm -i \"{}\"" }, + UpdateHandler { ".pkg.tar.zst", "sudo pacman -Syy && sudo pacman -U --noconfirm \"{}\"" } }; + const auto updateFileName = wolv::util::toUTF8String(updatePath.filename()); for (const auto &handler : UpdateHandlers) { - if (type == handler.type) { - // Rename the update file to the correct extension - const auto originalPath = updatePath; - updatePath.replace_extension(handler.extension); - - hex::log::info("Moving update package from {} to {}", originalPath.string(), updatePath.string()); - std::fs::rename(originalPath, updatePath); - + if (updateFileName.ends_with(handler.ending)) { // Install the update using the correct command const auto command = fmt::format(fmt::runtime(handler.command), updatePath.string()); + hex::log::info("Starting update process with command: '{}'", command); hex::startProgram(command); @@ -152,6 +169,8 @@ int installUpdate(const std::string &type, std::fs::path updatePath) { } int main(int argc, char **argv) { + hex::TaskManager::setCurrentThreadName("ImHex Updater"); + hex::TaskManager::setMainThreadId(std::this_thread::get_id()); hex::log::impl::enableColorPrinting(); // Check we have the correct number of arguments @@ -161,35 +180,50 @@ int main(int argc, char **argv) { } // Read the version type from the arguments - const auto versionType = argv[1]; - hex::log::info("Updater started with version type: {}", versionType); + const std::string_view versionTypeString = argv[1]; + hex::log::info("Updater started with version type: {}", versionTypeString); - // Query the update type - const auto updateType = getUpdateType(); - hex::log::info("Detected OS String: {}", updateType); - - // Make sure we got a valid update type - if (updateType.empty()) { - hex::log::error("Failed to detect installation type"); + // Convert the version type string to the enum value + hex::ImHexApi::System::UpdateType updateType; + if (versionTypeString == "stable") { + updateType = hex::ImHexApi::System::UpdateType::Stable; + } else if (versionTypeString == "nightly") { + updateType = hex::ImHexApi::System::UpdateType::Nightly; + } else { + hex::log::error("Invalid version type: {}", versionTypeString); return EXIT_FAILURE; } - // Get the url to the requested update from the ImHex API - const auto updateUrl = getUpdateUrl(versionType, updateType); - if (updateUrl.empty()) { - hex::log::error("Failed to get update URL"); + // Get the artifact name ending based on the current platform and architecture + const auto artifactEnding = getUpdateArtifactEnding(); + if (artifactEnding.empty()) { + hex::log::error("Updater artifact ending is empty"); return EXIT_FAILURE; } - hex::log::info("Update URL found: {}", updateUrl); + // Get the URL for the correct update artifact + const auto updateArtifactUrl = getArtifactUrl(artifactEnding, updateType); + if (updateArtifactUrl.empty()) { + // If the current artifact cannot be updated, open the latest release page in the browser + + hex::log::warn("Failed to get update artifact URL for ending: {}", artifactEnding); + hex::log::info("Opening release page in browser to allow manual update"); + + switch (updateType) { + case hex::ImHexApi::System::UpdateType::Stable: + hex::openWebpage("https://github.com/WerWolv/ImHex/releases/latest"); + break; + case hex::ImHexApi::System::UpdateType::Nightly: + hex::openWebpage("https://github.com/WerWolv/ImHex/releases/tag/nightly"); + break; + } - // Download the update file - const auto updatePath = downloadUpdate(updateUrl); - if (!updatePath.has_value()) return EXIT_FAILURE; + } - hex::log::info("Downloaded update successfully"); + // Download the update artifact + const auto updatePath = downloadUpdate(updateArtifactUrl); // Install the update - return installUpdate(updateType, *updatePath); + return installUpdate(*updatePath); } \ No newline at end of file diff --git a/plugins/builtin/source/content/init_tasks.cpp b/plugins/builtin/source/content/init_tasks.cpp index 863e0408f..934b4bd92 100644 --- a/plugins/builtin/source/content/init_tasks.cpp +++ b/plugins/builtin/source/content/init_tasks.cpp @@ -21,76 +21,122 @@ namespace hex::plugin::builtin { int checkForUpdates = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.server_contact", 2); // Check if we should check for updates - if (checkForUpdates == 1) { - HttpRequest request("GET", GitHubApiURL + "/releases/latest"s); + TaskManager::createBackgroundTask("Update Check", [checkForUpdates] { + std::string updateString; + if (checkForUpdates == 1) { + if (ImHexApi::System::isNightlyBuild()) { + HttpRequest request("GET", GitHubApiURL + "/releases/tags/nightly"s); + request.setTimeout(10000); - // Query the GitHub API for the latest release version - auto response = request.execute().get(); - if (response.getStatusCode() != 200) - return false; + // Query the GitHub API for the latest release version + auto response = request.execute().get(); + if (response.getStatusCode() != 200) + return; - nlohmann::json releases; - try { - releases = nlohmann::json::parse(response.getData()); - } catch (const std::exception &) { - return false; + nlohmann::json releases; + try { + releases = nlohmann::json::parse(response.getData()); + } catch (const std::exception &) { + return; + } + + // Check if the response is valid + if (!releases.contains("published_at") || !releases["published_at"].is_string()) + return; + + std::chrono::system_clock::time_point nightlyUpdateTime; + { + std::istringstream iss(releases["published_at"].get()); + iss >> std::chrono::parse("%FT%TZ", nightlyUpdateTime); + } + + if (nightlyUpdateTime > std::chrono::system_clock::now()) { + updateString = "Nightly"; + } + } else { + HttpRequest request("GET", GitHubApiURL + "/releases/latest"s); + + // Query the GitHub API for the latest release version + auto response = request.execute().get(); + if (response.getStatusCode() != 200) + return; + + nlohmann::json releases; + try { + releases = nlohmann::json::parse(response.getData()); + } catch (const std::exception &) { + return; + } + + // Check if the response is valid + if (!releases.contains("tag_name") || !releases["tag_name"].is_string()) + return; + + // Convert the current version string to a format that can be compared to the latest release + auto currVersion = "v" + ImHexApi::System::getImHexVersion().get(false); + + // Get the latest release version string + auto latestVersion = releases["tag_name"].get(); + + // Check if the latest release is different from the current version + if (latestVersion != currVersion) + updateString = latestVersion; + } } - // Check if the response is valid - if (!releases.contains("tag_name") || !releases["tag_name"].is_string()) - return false; + if (updateString.empty()) + return; - // Convert the current version string to a format that can be compared to the latest release - auto currVersion = "v" + ImHexApi::System::getImHexVersion().get(false); + TaskManager::doLater([updateString] { + ContentRegistry::Interface::addTitleBarButton(ICON_VS_ARROW_DOWN, ImGuiCustomCol_ToolbarGreen, "hex.builtin.welcome.update.title", [] { + ImHexApi::System::updateImHex(ImHexApi::System::isNightlyBuild() ? ImHexApi::System::UpdateType::Nightly : ImHexApi::System::UpdateType::Stable); + }); - // Get the latest release version string - auto latestVersion = releases["tag_name"].get(); - - // Check if the latest release is different from the current version - if (latestVersion != currVersion) - ImHexApi::System::impl::addInitArgument("update-available", latestVersion.data()); - - // Check if there is a telemetry uuid - auto uuid = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.uuid", ""); - if (uuid.empty()) { - // Generate a new uuid - uuid = wolv::hash::generateUUID(); - // Save - ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.uuid", uuid); - } - - TaskManager::createBackgroundTask("hex.builtin.task.sending_statistics", [uuid](auto&) { - // To avoid potentially flooding our database with lots of dead users - // from people just visiting the website, don't send telemetry data from - // the web version - #if defined(OS_WEB) - return; - #endif - - // Make telemetry request - nlohmann::json telemetry = { - { "uuid", uuid }, - { "format_version", "1" }, - { "imhex_version", ImHexApi::System::getImHexVersion().get(false) }, - { "imhex_commit", fmt::format("{}@{}", ImHexApi::System::getCommitHash(true), ImHexApi::System::getCommitBranch()) }, - { "install_type", ImHexApi::System::isPortableVersion() ? "Portable" : "Installed" }, - { "os", ImHexApi::System::getOSName() }, - { "os_version", ImHexApi::System::getOSVersion() }, - { "arch", ImHexApi::System::getArchitecture() }, - { "gpu_vendor", ImHexApi::System::getGPUVendor() }, - { "corporate_env", ImHexApi::System::isCorporateEnvironment() } - }; - - HttpRequest telemetryRequest("POST", ImHexApiURL + "/telemetry"s); - telemetryRequest.setTimeout(500); - - telemetryRequest.setBody(telemetry.dump()); - telemetryRequest.addHeader("Content-Type", "application/json"); - - // Execute request - telemetryRequest.execute(); + ui::ToastInfo::open(fmt::format("hex.builtin.welcome.update.desc"_lang, updateString)); }); + }); + + // Check if there is a telemetry uuid + auto uuid = ContentRegistry::Settings::read("hex.builtin.setting.general", "hex.builtin.setting.general.uuid", ""); + if (uuid.empty()) { + // Generate a new uuid + uuid = wolv::hash::generateUUID(); + // Save + ContentRegistry::Settings::write("hex.builtin.setting.general", "hex.builtin.setting.general.uuid", uuid); } + + TaskManager::createBackgroundTask("hex.builtin.task.sending_statistics", [uuid](auto&) { + // To avoid potentially flooding our database with lots of dead users + // from people just visiting the website, don't send telemetry data from + // the web version + #if defined(OS_WEB) + return; + #endif + + // Make telemetry request + nlohmann::json telemetry = { + { "uuid", uuid }, + { "format_version", "1" }, + { "imhex_version", ImHexApi::System::getImHexVersion().get(false) }, + { "imhex_commit", fmt::format("{}@{}", ImHexApi::System::getCommitHash(true), ImHexApi::System::getCommitBranch()) }, + { "install_type", ImHexApi::System::isPortableVersion() ? "Portable" : "Installed" }, + { "os", ImHexApi::System::getOSName() }, + { "os_version", ImHexApi::System::getOSVersion() }, + { "arch", ImHexApi::System::getArchitecture() }, + { "gpu_vendor", ImHexApi::System::getGPUVendor() }, + { "corporate_env", ImHexApi::System::isCorporateEnvironment() } + }; + + HttpRequest telemetryRequest("POST", ImHexApiURL + "/telemetry"s); + telemetryRequest.setTimeout(500); + + telemetryRequest.setBody(telemetry.dump()); + telemetryRequest.addHeader("Content-Type", "application/json"); + + // Execute request + telemetryRequest.execute(); + }); + return true; } diff --git a/plugins/builtin/source/content/ui_items.cpp b/plugins/builtin/source/content/ui_items.cpp index 81af36ef2..577b6273e 100644 --- a/plugins/builtin/source/content/ui_items.cpp +++ b/plugins/builtin/source/content/ui_items.cpp @@ -28,7 +28,7 @@ namespace hex::plugin::builtin { void addTitleBarButtons() { if (dbg::debugModeEnabled()) { - ContentRegistry::Interface::addTitleBarButton(ICON_VS_DEBUG, "hex.builtin.title_bar_button.debug_build", []{ + ContentRegistry::Interface::addTitleBarButton(ICON_VS_DEBUG, ImGuiCustomCol_ToolbarGray, "hex.builtin.title_bar_button.debug_build", []{ if (ImGui::GetIO().KeyShift) { RequestOpenPopup::post("DebugMenu"); } else { @@ -37,7 +37,7 @@ namespace hex::plugin::builtin { }); } - ContentRegistry::Interface::addTitleBarButton(ICON_VS_SMILEY, "hex.builtin.title_bar_button.feedback", []{ + ContentRegistry::Interface::addTitleBarButton(ICON_VS_SMILEY, ImGuiCustomCol_ToolbarGray, "hex.builtin.title_bar_button.feedback", []{ hex::openWebpage("https://github.com/WerWolv/ImHex/discussions/categories/feedback"); }); @@ -555,15 +555,6 @@ namespace hex::plugin::builtin { ContentRegistry::Interface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.file.save", ImGuiCustomCol_ToolbarBlue); ContentRegistry::Interface::addMenuItemToToolbar("hex.builtin.view.hex_editor.menu.file.save_as", ImGuiCustomCol_ToolbarBlue); ContentRegistry::Interface::addMenuItemToToolbar("hex.builtin.menu.edit.bookmark.create", ImGuiCustomCol_ToolbarGreen); - - const auto &initArgs = ImHexApi::System::getInitArguments(); - if (auto it = initArgs.find("update-available"); it != initArgs.end()) { - ContentRegistry::Interface::addTitleBarButton(ICON_VS_GIFT, "hex.builtin.welcome.update.title", [] { - ImHexApi::System::updateImHex(ImHexApi::System::UpdateType::Stable); - }); - - ui::ToastInfo::open(fmt::format("hex.builtin.welcome.update.desc"_lang, it->second)); - } }); } diff --git a/plugins/builtin/source/content/window_decoration.cpp b/plugins/builtin/source/content/window_decoration.cpp index f856cfbc9..686862035 100644 --- a/plugins/builtin/source/content/window_decoration.cpp +++ b/plugins/builtin/source/content/window_decoration.cpp @@ -232,18 +232,20 @@ namespace hex::plugin::builtin { #endif } - auto &titleBarButtons = ContentRegistry::Interface::impl::getTitlebarButtons(); + const auto &titleBarButtons = ContentRegistry::Interface::impl::getTitlebarButtons(); // Draw custom title bar buttons if (!titleBarButtons.empty()) { ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 7_scaled - (buttonSize.x + ImGui::GetStyle().ItemSpacing.x) * float((titleBarButtonsVisible ? 4 : 0) + titleBarButtons.size())); if (ImGui::GetCursorPosX() > (searchBoxPos.x + searchBoxSize.x)) { - for (const auto &[icon, tooltip, callback] : titleBarButtons) { + for (const auto &[icon, color, tooltip, callback] : titleBarButtons) { ImGui::SetCursorPosY(titleBarButtonPosY); + ImGui::PushStyleColor(ImGuiCol_Text, ImGuiExt::GetCustomColorVec4(color)); if (ImGuiExt::TitleBarButton(icon.c_str(), buttonSize)) { callback(); } + ImGui::PopStyleColor(); ImGuiExt::InfoTooltip(Lang(tooltip)); } }