From 5f785c82dd406a5e7ca3934666cc7e1d7a2cb5b2 Mon Sep 17 00:00:00 2001 From: devmuaz Date: Thu, 7 May 2026 14:47:45 +0300 Subject: [PATCH 1/2] Add skin management module and window, log filtering for AVD logs, and refactor theme system and expand preferences --- CMakeLists.txt | 3 + src/core/avd.cpp | 10 + src/core/avd.h | 3 + src/core/log_filter.cpp | 78 +++++++ src/core/log_filter.h | 34 +++ src/core/skin.cpp | 151 +++++++++++++ src/core/skin.h | 35 +++ src/gui/context.h | 19 +- src/gui/icons.h | 31 --- src/gui/theme.cpp | 88 ++++---- src/gui/theme.h | 62 ++++++ src/gui/widgets.cpp | 134 ++++++----- src/gui/widgets.h | 2 + src/gui/windows/about.cpp | 10 +- src/gui/windows/avd_info.cpp | 6 +- src/gui/windows/avd_list.cpp | 20 +- src/gui/windows/avd_logs.cpp | 344 ++++++++++++++++++++++------- src/gui/windows/avd_options.cpp | 3 +- src/gui/windows/create_avd.cpp | 47 +++- src/gui/windows/device_profile.cpp | 17 +- src/gui/windows/install_image.cpp | 21 +- src/gui/windows/main_menu_bar.cpp | 12 +- src/gui/windows/onboarding.cpp | 20 +- src/gui/windows/preferences.cpp | 251 +++++++++++++++++---- src/gui/windows/skin.cpp | 152 +++++++++++++ src/gui/windows/skin.h | 18 ++ src/gui/windows/storage.cpp | 20 +- src/gui/windows/update.cpp | 6 +- tests/CMakeLists.txt | 2 + tests/test_log_filter.cpp | 92 ++++++++ tests/test_skin.cpp | 107 +++++++++ 31 files changed, 1473 insertions(+), 325 deletions(-) create mode 100644 src/core/log_filter.cpp create mode 100644 src/core/log_filter.h create mode 100644 src/core/skin.cpp create mode 100644 src/core/skin.h delete mode 100644 src/gui/icons.h create mode 100644 src/gui/windows/skin.cpp create mode 100644 src/gui/windows/skin.h create mode 100644 tests/test_log_filter.cpp create mode 100644 tests/test_skin.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d25aa0..c5190da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,10 +132,12 @@ add_library(coredeck_core STATIC src/core/emulator_console.cpp src/core/file_dialog.cpp src/core/log_buffer.cpp + src/core/log_filter.cpp src/core/options.cpp src/core/paths.cpp src/core/process.cpp src/core/sdk.cpp + src/core/skin.cpp src/core/system_image.cpp src/core/utilities.cpp src/core/version_check.cpp @@ -189,6 +191,7 @@ add_executable(${PROJECT_NAME} src/gui/windows/onboarding.cpp src/gui/windows/preferences.cpp src/gui/windows/sdk_banner.cpp + src/gui/windows/skin.cpp src/gui/windows/storage.cpp src/gui/windows/update.cpp src/gui/application.cpp diff --git a/src/core/avd.cpp b/src/core/avd.cpp index dc60a21..534832c 100644 --- a/src/core/avd.cpp +++ b/src/core/avd.cpp @@ -190,6 +190,10 @@ namespace CoreDeck { avd.GpuMode = it->second; } + if (auto it = config.find("skin.name"); it != config.end()) { + avd.SkinName = it->second; + } + return avd; } @@ -255,6 +259,12 @@ namespace CoreDeck { file << "hw.gpu.mode=" << data.GpuMode << "\n"; file << "hw.gpu.enabled=yes\n"; } + if (!data.SkinName.empty()) { + file << "skin.name=" << data.SkinName << "\n"; + if (!data.SkinPath.empty()) { + file << "skin.path=" << data.SkinPath << "\n"; + } + } } } diff --git a/src/core/avd.h b/src/core/avd.h index 30edddb..8838c3c 100644 --- a/src/core/avd.h +++ b/src/core/avd.h @@ -32,6 +32,7 @@ namespace CoreDeck { std::string GpuMode; std::string Arch; std::string Path; + std::string SkinName; }; struct AvdCreationData { @@ -42,6 +43,8 @@ namespace CoreDeck { std::string RamSize; std::string SdCardSize; std::string GpuMode; + std::string SkinName; + std::string SkinPath; }; std::vector LoadAvds(const std::vector &avdNames); diff --git a/src/core/log_filter.cpp b/src/core/log_filter.cpp new file mode 100644 index 0000000..ad7ba9f --- /dev/null +++ b/src/core/log_filter.cpp @@ -0,0 +1,78 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#include + +#include "log_filter.h" +#include "utilities.h" + +namespace CoreDeck { + static void AppendLine(std::string &out, const std::string &line) { + out.append(line); + out.push_back('\n'); + } + + static void CollectSubstringMatches(const std::string &line, const std::size_t &lineStart, const std::string &needle, const bool &caseSensitive, std::vector &out) { + if (needle.empty()) return; + + const std::string haystack = caseSensitive ? line : LowerCopy(line); + const std::string pattern = caseSensitive ? needle : LowerCopy(needle); + + std::size_t pos = 0; + while ((pos = haystack.find(pattern, pos)) != std::string::npos) { + LogMatch m; + m.StartOffset = lineStart + pos; + m.EndOffset = m.StartOffset + pattern.size(); + out.push_back(m); + pos += pattern.size(); + } + } + + static void CollectRegexMatches(const std::string &line, const std::size_t lineStart, const std::regex &re, std::vector &out) { + const auto end = std::sregex_iterator{}; + for (auto it = std::sregex_iterator(line.begin(), line.end(), re); it != end; ++it) { + if (it->length() == 0) continue; + LogMatch m; + m.StartOffset = lineStart + static_cast(it->position()); + m.EndOffset = m.StartOffset + static_cast(it->length()); + out.push_back(m); + } + } + + + LogFilterResult FilterLog(const std::vector &lines, const LogFilterOptions &options) { + LogFilterResult result; + result.Joined.reserve(lines.size() * 80); + + const bool hasQuery = !options.Query.empty(); + + std::regex compiled; + if (hasQuery && options.UseRegex) { + try { + auto flags = std::regex::ECMAScript; + if (!options.CaseSensitive) flags |= std::regex::icase; + compiled = std::regex(options.Query, flags); + } catch (const std::regex_error &e) { + result.RegexValid = false; + result.RegexError = e.what(); + } + } + + const bool collectMatches = hasQuery && (options.UseRegex ? result.RegexValid : true); + + for (const auto &line: lines) { + const std::size_t lineStart = result.Joined.size(); + if (collectMatches) { + if (options.UseRegex) { + CollectRegexMatches(line, lineStart, compiled, result.Matches); + } else { + CollectSubstringMatches(line, lineStart, options.Query, options.CaseSensitive, result.Matches); + } + } + AppendLine(result.Joined, line); + } + + return result; + } +} \ No newline at end of file diff --git a/src/core/log_filter.h b/src/core/log_filter.h new file mode 100644 index 0000000..98668dd --- /dev/null +++ b/src/core/log_filter.h @@ -0,0 +1,34 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#ifndef COREDECK_LOG_FILTER_H +#define COREDECK_LOG_FILTER_H + +#include +#include +#include + +namespace CoreDeck { + struct LogFilterOptions { + std::string Query; + bool UseRegex = false; + bool CaseSensitive = false; + }; + + struct LogMatch { + std::size_t StartOffset = 0; + std::size_t EndOffset = 0; + }; + + struct LogFilterResult { + std::string Joined; + std::vector Matches; + bool RegexValid = true; + std::string RegexError; + }; + + LogFilterResult FilterLog(const std::vector &lines, const LogFilterOptions &options); +} + +#endif // COREDECK_LOG_FILTER_H \ No newline at end of file diff --git a/src/core/skin.cpp b/src/core/skin.cpp new file mode 100644 index 0000000..37610d8 --- /dev/null +++ b/src/core/skin.cpp @@ -0,0 +1,151 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#include +#include +#include +#include + +#include "skin.h" +#include "paths.h" +#include "utilities.h" + +#include + +namespace CoreDeck { + namespace fs = std::filesystem; + + static std::string PrettifyName(const std::string &id) { + std::string out = id; + std::ranges::replace(out, '_', ' '); + bool atStart = true; + for (auto &c: out) { + if (atStart && std::isalpha(static_cast(c))) { + c = static_cast(std::toupper(static_cast(c))); + atStart = false; + } else if (c == ' ') { + atStart = true; + } + } + return out; + } + + static bool IsSkinDirectory(const fs::path &dir) { + std::error_code ec; + if (!fs::is_directory(dir, ec)) return false; + return fs::exists(dir / "layout", ec); + } + + static void CollectSkinsFrom(const fs::path &root, const SkinSource &source, std::vector &out) { + std::error_code ec; + if (!fs::is_directory(root, ec)) return; + + for (const auto &entry: fs::directory_iterator(root, ec)) { + if (ec) break; + if (!entry.is_directory(ec)) continue; + const auto &dir = entry.path(); + if (!IsSkinDirectory(dir)) continue; + + std::string dirName = dir.filename().string(); + out.emplace_back( + dirName, + PrettifyName(dirName), + dir.string(), + source + ); + } + } + + static int SourcePriority(const SkinSource &source) { + switch (source) { + case SkinSource::Sdk: + return 0; + case SkinSource::SystemImage: + return 1; + case SkinSource::Platform: + return 2; + } + return 99; + } + + std::vector ListSkins(const SdkInfo &sdk) { + std::vector all; + if (sdk.SdkPath.empty()) return all; + + CollectSkinsFrom(fs::path(sdk.SdkPath) / "skins", SkinSource::Sdk, all); + + const fs::path sysImgRoot = fs::path(sdk.SdkPath) / "system-images"; + std::error_code ec; + if (fs::is_directory(sysImgRoot, ec)) { + for (const auto &api: fs::directory_iterator(sysImgRoot, ec)) { + if (!api.is_directory(ec)) continue; + for (const auto &variant: fs::directory_iterator(api.path(), ec)) { + if (!variant.is_directory(ec)) continue; + for (const auto &abi: fs::directory_iterator(variant.path(), ec)) { + if (!abi.is_directory(ec)) continue; + CollectSkinsFrom(abi.path() / "skins", SkinSource::SystemImage, all); + } + } + } + } + + const fs::path platformsRoot = fs::path(sdk.SdkPath) / "platforms"; + if (fs::is_directory(platformsRoot, ec)) { + for (const auto &platform: fs::directory_iterator(platformsRoot, ec)) { + if (!platform.is_directory(ec)) continue; + CollectSkinsFrom(platform.path() / "skins", SkinSource::Platform, all); + } + } + + std::unordered_map bestByName; + for (size_t i = 0; i < all.size(); ++i) { + const std::string key = LowerCopy(all[i].Name); + auto it = bestByName.find(key); + if (it == bestByName.end()) { + bestByName.emplace(key, i); + } else if (SourcePriority(all[i].Source) < SourcePriority(all[it->second].Source)) { + it->second = i; + } + } + + std::vector deduped; + deduped.reserve(bestByName.size()); + for (auto &idx: bestByName | std::views::values) deduped.push_back(std::move(all[idx])); + + std::ranges::sort(deduped, [](const Skin &a, const Skin &b) { + return LowerCopy(a.DisplayName) < LowerCopy(b.DisplayName); + }); + + return deduped; + } + + std::optional FindSkinForDevice(const std::vector &skins, const std::string &deviceId) { + if (deviceId.empty() || skins.empty()) return std::nullopt; + + const std::string needle = LowerCopy(deviceId); + + for (const auto &s: skins) { + if (LowerCopy(s.Name) == needle) return s; + } + for (const auto &s: skins) { + const std::string lower = LowerCopy(s.Name); + if (lower.find(needle) != std::string::npos || needle.find(lower) != std::string::npos) { + return s; + } + } + return std::nullopt; + } + + const char *SkinSourceLabel(const SkinSource &source) { + switch (source) { + case SkinSource::Sdk: + return "SDK"; + case SkinSource::SystemImage: + return "System image"; + case SkinSource::Platform: + return "Platform"; + } + return ""; + } +} \ No newline at end of file diff --git a/src/core/skin.h b/src/core/skin.h new file mode 100644 index 0000000..84566f5 --- /dev/null +++ b/src/core/skin.h @@ -0,0 +1,35 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#ifndef COREDECK_SKIN_H +#define COREDECK_SKIN_H + +#include +#include +#include + +#include "sdk.h" + +namespace CoreDeck { + enum class SkinSource { + Sdk, + SystemImage, + Platform, + }; + + struct Skin { + std::string Name; + std::string DisplayName; + std::string Path; + SkinSource Source = SkinSource::Sdk; + }; + + std::vector ListSkins(const SdkInfo &sdk); + + std::optional FindSkinForDevice(const std::vector &skins, const std::string &deviceId); + + const char *SkinSourceLabel(const SkinSource &source); +} + +#endif // COREDECK_SKIN_H \ No newline at end of file diff --git a/src/gui/context.h b/src/gui/context.h index feb761a..059dc8b 100644 --- a/src/gui/context.h +++ b/src/gui/context.h @@ -17,6 +17,7 @@ #include "../core/emulator.h" #include "../core/options.h" #include "../core/sdk.h" +#include "../core/skin.h" #include "../core/system_image.h" struct GLFWwindow; @@ -86,9 +87,18 @@ namespace CoreDeck { std::vector FilteredIndices; } Catalog; + struct LogViewState { + std::string Search; + int ActiveMatchIndex = 0; + bool UseRegex = false; + }; + struct Logs { - std::unordered_map PerAvdLogSearch; + std::unordered_map PerAvdView; bool AutoScroll = true; + bool PendingScroll = false; + bool PendingFocus = false; + int PendingSyncFrames = 0; } Logs; struct Prefs { @@ -100,6 +110,7 @@ namespace CoreDeck { bool ShowDeleteAvdDialog = false; bool ShowCreateAvdDialog = false; bool ShowDeviceProfileDialog = false; + bool ShowSkinDialog = false; bool ShowInstallImageDialog = false; bool ReopenCreateAvdOnInstallClose = false; bool ShowPreferences = false; @@ -116,15 +127,21 @@ namespace CoreDeck { struct AvdCreationWork { std::vector SystemImages; std::vector DeviceProfiles; + std::vector Skins; AvdCreationData CreationData; int SelectedSystemImage = 0; int SelectedDevice = 0; int PendingSelectedDevice = 0; + int SelectedSkin = 0; // 0 = "No skin" + int PendingSelectedSkin = 0; + char SkinSearchFilter[128] = {}; DeviceCategory SelectedDeviceCategory = DeviceCategory::Phone; char DeviceSearchFilter[128] = {}; int SelectedGpuMode = 0; bool NameAutoFilled = true; bool DisplayNameAutoFilled = true; + bool SkinAutoFilled = true; + int LastDeviceForSkinAuto = -1; struct { std::atomic Loading{false}; diff --git a/src/gui/icons.h b/src/gui/icons.h deleted file mode 100644 index 583b5a4..0000000 --- a/src/gui/icons.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// Created by AbdulMuaz Aqeel on 24/04/2026. -// - -#ifndef COREDECK_ICONS_H -#define COREDECK_ICONS_H - -namespace CoreDeck::Icons { - constexpr const char *Play = "\xef\x81\x8b"; // fa-play (f04b) - constexpr const char *Stop = "\xef\x81\x8d"; // fa-stop (f04d) - constexpr const char *Refresh = "\xef\x80\xa1"; // fa-arrows-rotate (f021) - constexpr const char *Trash = "\xef\x87\xb8"; // fa-trash-can (f1f8) - constexpr const char *Circle = "\xef\x84\x91"; // fa-circle (f111) - constexpr const char *Desktop = "\xef\x84\x88"; // fa-desktop (f108) - constexpr const char *Gear = "\xef\x80\x93"; // fa-gear (f013) - constexpr const char *Terminal = "\xef\x84\xa0"; // fa-terminal (f120) - constexpr const char *Info = "\xef\x81\x9a"; // fa-circle-info (f05a) - constexpr const char *Search = "\xef\x80\x82"; // fa-magnifying-glass (f002) - constexpr const char *Plus = "\xef\x81\xa7"; // fa-plus (f067) - constexpr const char *SortUp = "\xef\x83\x9e"; // fa-sort-up (f0de) - constexpr const char *SortDown = "\xef\x83\x9d"; // fa-sort-down (f0dd) - constexpr const char *Sort = "\xef\x83\x9c"; // fa-sort (f0dc) - constexpr const char *Times = "\xef\x80\x8d"; // fa-xmark (f00d) - constexpr const char *Mobile = "\xef\x8f\x8d"; // fa-mobile-screen-button (f3cd) - constexpr const char *Tablet = "\xef\x8f\xba"; // fa-tablet-screen-button (f3fa) - constexpr const char *Tv = "\xef\x89\xac"; // fa-tv (f26c) - constexpr const char *Watch = "\xef\x80\x97"; // fa-clock (f017) — used for Wear OS - constexpr const char *Car = "\xef\x86\xb9"; // fa-car (f1b9) — used for Automotive -} - -#endif // COREDECK_ICONS_H diff --git a/src/gui/theme.cpp b/src/gui/theme.cpp index f916fd8..f475898 100644 --- a/src/gui/theme.cpp +++ b/src/gui/theme.cpp @@ -28,81 +28,81 @@ namespace CoreDeck { auto &c = style.Colors; // Dock tabs — inactive - c[ImGuiCol_Tab] = HexColor("#000000", 0.0f); - c[ImGuiCol_TabHovered] = HexColor("#000000", 0.0f); - c[ImGuiCol_TabSelected] = HexColor("#000000", 0.0f); - c[ImGuiCol_TabSelectedOverline] = HexColor("#000000", 0.0f); + c[ImGuiCol_Tab] = HexColor(Colors::Shadow, 0.0f); + c[ImGuiCol_TabHovered] = HexColor(Colors::Shadow, 0.0f); + c[ImGuiCol_TabSelected] = HexColor(Colors::Shadow, 0.0f); + c[ImGuiCol_TabSelectedOverline] = HexColor(Colors::Shadow, 0.0f); // Dock tabs — unfocused window - c[ImGuiCol_TabDimmed] = HexColor("#000000", 0.0f); - c[ImGuiCol_TabDimmedSelected] = HexColor("#000000", 0.0f); - c[ImGuiCol_TabDimmedSelectedOverline] = HexColor("#000000", 0.0f); + c[ImGuiCol_TabDimmed] = HexColor(Colors::Shadow, 0.0f); + c[ImGuiCol_TabDimmedSelected] = HexColor(Colors::Shadow, 0.0f); + c[ImGuiCol_TabDimmedSelectedOverline] = HexColor(Colors::Shadow, 0.0f); // Docking preview overlay - c[ImGuiCol_DockingPreview] = HexColor("#F2F2F2", 0.20f); - c[ImGuiCol_DockingEmptyBg] = HexColor("#0A0A0C"); + c[ImGuiCol_DockingPreview] = HexColor(Colors::TextPrimary, 0.20f); + c[ImGuiCol_DockingEmptyBg] = HexColor(Colors::Surface0); // Window - c[ImGuiCol_WindowBg] = HexColor("#0F0F12"); - c[ImGuiCol_ChildBg] = HexColor("#0F0F12"); - c[ImGuiCol_PopupBg] = HexColor("#141417", 0.98f); - c[ImGuiCol_ModalWindowDimBg] = HexColor("#000000", 0.55f); + c[ImGuiCol_WindowBg] = HexColor(Colors::Surface0); + c[ImGuiCol_ChildBg] = HexColor(Colors::Surface0); + c[ImGuiCol_PopupBg] = HexColor(Colors::Surface1, 0.98f); + c[ImGuiCol_ModalWindowDimBg] = HexColor(Colors::Shadow, 0.55f); // Borders - c[ImGuiCol_Border] = HexColor("#2E2E33"); - c[ImGuiCol_BorderShadow] = HexColor("#000000", 0.0f); + c[ImGuiCol_Border] = HexColor(Colors::Surface4); + c[ImGuiCol_BorderShadow] = HexColor(Colors::Shadow, 0.0f); // Text - c[ImGuiCol_Text] = HexColor("#F2F2F2"); - c[ImGuiCol_TextDisabled] = HexColor("#66666B"); + c[ImGuiCol_Text] = HexColor(Colors::TextPrimary); + c[ImGuiCol_TextDisabled] = HexColor(Colors::TextMuted); // Headers - c[ImGuiCol_Header] = HexColor("#1F1F21"); - c[ImGuiCol_HeaderHovered] = HexColor("#29292B"); - c[ImGuiCol_HeaderActive] = HexColor("#333336"); + c[ImGuiCol_Header] = HexColor(Colors::Surface3); + c[ImGuiCol_HeaderHovered] = HexColor(Colors::Surface3); + c[ImGuiCol_HeaderActive] = HexColor(Colors::Surface4); // Buttons - c[ImGuiCol_Button] = HexColor("#1A1A1C"); - c[ImGuiCol_ButtonHovered] = HexColor("#2E2E30"); - c[ImGuiCol_ButtonActive] = HexColor("#0F0F12"); + c[ImGuiCol_Button] = HexColor(Colors::Surface2); + c[ImGuiCol_ButtonHovered] = HexColor(Colors::Surface4); + c[ImGuiCol_ButtonActive] = HexColor(Colors::Surface0); // Frame - c[ImGuiCol_FrameBg] = HexColor("#141417"); - c[ImGuiCol_FrameBgHovered] = HexColor("#1F1F21"); - c[ImGuiCol_FrameBgActive] = HexColor("#242426"); + c[ImGuiCol_FrameBg] = HexColor(Colors::Surface1); + c[ImGuiCol_FrameBgHovered] = HexColor(Colors::Surface3); + c[ImGuiCol_FrameBgActive] = HexColor(Colors::Surface3); // Checkbox - c[ImGuiCol_CheckMark] = HexColor("#F2F2F2"); + c[ImGuiCol_CheckMark] = HexColor(Colors::TextPrimary); // Slider - c[ImGuiCol_SliderGrab] = HexColor("#F2F2F2"); - c[ImGuiCol_SliderGrabActive] = HexColor("#CCCCCC"); + c[ImGuiCol_SliderGrab] = HexColor(Colors::TextPrimary); + c[ImGuiCol_SliderGrabActive] = HexColor(Colors::TextOnDark); // Scrollbar - c[ImGuiCol_ScrollbarBg] = HexColor("#0F0F12"); - c[ImGuiCol_ScrollbarGrab] = HexColor("#333336"); - c[ImGuiCol_ScrollbarGrabHovered] = HexColor("#47474A"); - c[ImGuiCol_ScrollbarGrabActive] = HexColor("#5C5C5E"); + c[ImGuiCol_ScrollbarBg] = HexColor(Colors::Surface0); + c[ImGuiCol_ScrollbarGrab] = HexColor(Colors::Surface4); + c[ImGuiCol_ScrollbarGrabHovered] = HexColor(Colors::Border); + c[ImGuiCol_ScrollbarGrabActive] = HexColor(Colors::BorderHover); // Separator - c[ImGuiCol_Separator] = HexColor("#1A1A1E"); - c[ImGuiCol_SeparatorHovered] = HexColor("#4D4D4F"); - c[ImGuiCol_SeparatorActive] = HexColor("#666669"); + c[ImGuiCol_Separator] = HexColor(Colors::Surface2); + c[ImGuiCol_SeparatorHovered] = HexColor(Colors::BorderStrong); + c[ImGuiCol_SeparatorActive] = HexColor(Colors::TextMuted); // Menu bar - c[ImGuiCol_MenuBarBg] = HexColor("#0A0A0C"); + c[ImGuiCol_MenuBarBg] = HexColor(Colors::Surface0); // Title bar - c[ImGuiCol_TitleBg] = HexColor("#0F0F12"); - c[ImGuiCol_TitleBgActive] = HexColor("#141417"); - c[ImGuiCol_TitleBgCollapsed] = HexColor("#0F0F12"); + c[ImGuiCol_TitleBg] = HexColor(Colors::Surface0); + c[ImGuiCol_TitleBgActive] = HexColor(Colors::Surface1); + c[ImGuiCol_TitleBgCollapsed] = HexColor(Colors::Surface0); // Text selection - c[ImGuiCol_TextSelectedBg] = HexColor("#3F3F42", 0.60f); + c[ImGuiCol_TextSelectedBg] = HexColor(Colors::BorderSubtle, 0.60f); // Resize grip - c[ImGuiCol_ResizeGrip] = HexColor("#333336", 0.25f); - c[ImGuiCol_ResizeGripHovered] = HexColor("#4D4D4F", 0.65f); - c[ImGuiCol_ResizeGripActive] = HexColor("#666669", 0.95f); + c[ImGuiCol_ResizeGrip] = HexColor(Colors::Surface4, 0.25f); + c[ImGuiCol_ResizeGripHovered] = HexColor(Colors::BorderStrong, 0.65f); + c[ImGuiCol_ResizeGripActive] = HexColor(Colors::TextMuted, 0.95f); } } \ No newline at end of file diff --git a/src/gui/theme.h b/src/gui/theme.h index 7f9e073..4cf02d8 100644 --- a/src/gui/theme.h +++ b/src/gui/theme.h @@ -8,6 +8,68 @@ #include "imgui.h" namespace CoreDeck { + namespace Icons { + constexpr const char *Play = "\xef\x81\x8b"; + constexpr const char *Stop = "\xef\x81\x8d"; + constexpr const char *Refresh = "\xef\x80\xa1"; + constexpr const char *Trash = "\xef\x87\xb8"; + constexpr const char *Circle = "\xef\x84\x91"; + constexpr const char *Desktop = "\xef\x84\x88"; + constexpr const char *Gear = "\xef\x80\x93"; + constexpr const char *Terminal = "\xef\x84\xa0"; + constexpr const char *Info = "\xef\x81\x9a"; + constexpr const char *Search = "\xef\x80\x82"; + constexpr const char *Plus = "\xef\x81\xa7"; + constexpr const char *SortUp = "\xef\x83\x9e"; + constexpr const char *SortDown = "\xef\x83\x9d"; + constexpr const char *Sort = "\xef\x83\x9c"; + constexpr const char *Times = "\xef\x80\x8d"; + constexpr const char *Mobile = "\xef\x8f\x8d"; + constexpr const char *Tablet = "\xef\x8f\xba"; + constexpr const char *Tv = "\xef\x89\xac"; + constexpr const char *Watch = "\xef\x80\x97"; + constexpr const char *Car = "\xef\x86\xb9"; + constexpr const char *Copy = "\xef\x83\x85"; + constexpr const char *ChevronLeft = "\xef\x81\x93"; + constexpr const char *ChevronRight = "\xef\x81\x94"; + } + + namespace Colors { + constexpr const char *White = "#FFFFFF"; + constexpr const char *Positive = "#33CC47"; + constexpr const char *PositiveFill = "#26B333"; + constexpr const char *Negative = "#E64D40"; + constexpr const char *NegativeStrong = "#CC261F"; + constexpr const char *Warning = "#D9B31A"; + constexpr const char *WarningStrong = "#E6BF26"; + + constexpr const char *AccentPhone = "#4FC3F7"; + constexpr const char *AccentTablet = "#22D3EE"; + constexpr const char *AccentWear = "#F5A623"; + constexpr const char *AccentTv = "#7E57C2"; + constexpr const char *AccentInfo = "#4D9AFF"; + constexpr const char *AccentInfoSoft = "#7AB8FF"; + + constexpr const char *TextPrimary = "#F2F2F2"; + constexpr const char *TextMuted = "#66666B"; + constexpr const char *TextSubtle = "#A7A7AD"; + constexpr const char *TextOnDark = "#CCCCCC"; + constexpr const char *TextOnBright = "#969696"; + constexpr const char *TextHint = "#CFCFD4"; + + constexpr const char *Shadow = "#000000"; + constexpr const char *Surface0 = "#0F0F12"; + constexpr const char *Surface1 = "#141417"; + constexpr const char *Surface2 = "#1A1A1C"; + constexpr const char *Surface3 = "#29292B"; + constexpr const char *Surface4 = "#2E2E33"; + + constexpr const char *BorderSubtle = "#3F3F42"; + constexpr const char *Border = "#47474A"; + constexpr const char *BorderStrong = "#4D4D4F"; + constexpr const char *BorderHover = "#5C5C5E"; + } + void ApplyCustomImGuiTheme(); constexpr ImVec4 HexColor(const char *hex, float alpha = 1.0f) { diff --git a/src/gui/widgets.cpp b/src/gui/widgets.cpp index 588c614..9df645d 100644 --- a/src/gui/widgets.cpp +++ b/src/gui/widgets.cpp @@ -9,16 +9,16 @@ namespace CoreDeck { PickerTableStyle::PickerTableStyle() { - Colors.push(ImGuiCol_ChildBg, HexColor("#141417")); - Colors.push(ImGuiCol_Border, HexColor("#2E2E33")); - Colors.push(ImGuiCol_TableHeaderBg, HexColor("#1A1A1C")); - Colors.push(ImGuiCol_TableRowBg, HexColor("#000000", 0.0f)); - Colors.push(ImGuiCol_TableRowBgAlt, HexColor("#1A1A1C", 0.28f)); - Colors.push(ImGuiCol_TableBorderLight, HexColor("#242428")); - Colors.push(ImGuiCol_TableBorderStrong, HexColor("#2E2E33")); - Colors.push(ImGuiCol_Header, HexColor("#29292B", 0.65f)); - Colors.push(ImGuiCol_HeaderHovered, HexColor("#333336", 0.85f)); - Colors.push(ImGuiCol_HeaderActive, HexColor("#3F3F42")); + Colors.push(ImGuiCol_ChildBg, HexColor(Colors::Surface1)); + Colors.push(ImGuiCol_Border, HexColor(Colors::Surface4)); + Colors.push(ImGuiCol_TableHeaderBg, HexColor(Colors::Surface2)); + Colors.push(ImGuiCol_TableRowBg, HexColor(Colors::Shadow, 0.0f)); + Colors.push(ImGuiCol_TableRowBgAlt, HexColor(Colors::Surface2, 0.28f)); + Colors.push(ImGuiCol_TableBorderLight, HexColor(Colors::Surface3)); + Colors.push(ImGuiCol_TableBorderStrong, HexColor(Colors::Surface4)); + Colors.push(ImGuiCol_Header, HexColor(Colors::Surface3, 0.65f)); + Colors.push(ImGuiCol_HeaderHovered, HexColor(Colors::Surface4, 0.85f)); + Colors.push(ImGuiCol_HeaderActive, HexColor(Colors::BorderSubtle)); Vars.push(ImGuiStyleVar_ChildRounding, 6.0f); Vars.push(ImGuiStyleVar_ChildBorderSize, 1.0f); @@ -30,11 +30,11 @@ namespace CoreDeck { if (!isEnabled) ImGui::BeginDisabled(); StyleColor sc; - sc.push(ImGuiCol_Button, HexColor("#1A1A1C")); - sc.push(ImGuiCol_ButtonHovered, HexColor("#2E2E30", 0.6f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#0F0F12")); - sc.push(ImGuiCol_Text, HexColor("#F2F2F2")); - sc.push(ImGuiCol_Border, HexColor("#4D4D52")); + sc.push(ImGuiCol_Button, HexColor(Colors::Surface2)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::Surface4, 0.6f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::Surface0)); + sc.push(ImGuiCol_Text, HexColor(Colors::TextPrimary)); + sc.push(ImGuiCol_Border, HexColor(Colors::BorderStrong)); const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); @@ -45,11 +45,11 @@ namespace CoreDeck { if (!isEnabled) ImGui::BeginDisabled(); StyleColor sc; - sc.push(ImGuiCol_Button, HexColor("#CC261F", 0.10f)); - sc.push(ImGuiCol_ButtonHovered, HexColor("#CC261F", 0.20f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#CC261F", 0.30f)); - sc.push(ImGuiCol_Text, HexColor("#E64D40")); - sc.push(ImGuiCol_Border, HexColor("#E64D40")); + sc.push(ImGuiCol_Button, HexColor(Colors::NegativeStrong, 0.10f)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::NegativeStrong, 0.20f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::NegativeStrong, 0.30f)); + sc.push(ImGuiCol_Text, HexColor(Colors::Negative)); + sc.push(ImGuiCol_Border, HexColor(Colors::Negative)); const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); @@ -60,11 +60,11 @@ namespace CoreDeck { if (!isEnabled) ImGui::BeginDisabled(); StyleColor sc; - sc.push(ImGuiCol_Button, HexColor("#D9B31A", 0.10f)); - sc.push(ImGuiCol_ButtonHovered, HexColor("#D9B31A", 0.20f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#D9B31A", 0.30f)); - sc.push(ImGuiCol_Text, HexColor("#E6BF26")); - sc.push(ImGuiCol_Border, HexColor("#E6BF26")); + sc.push(ImGuiCol_Button, HexColor(Colors::Warning, 0.10f)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::Warning, 0.20f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::Warning, 0.30f)); + sc.push(ImGuiCol_Text, HexColor(Colors::WarningStrong)); + sc.push(ImGuiCol_Border, HexColor(Colors::WarningStrong)); const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); @@ -75,11 +75,11 @@ namespace CoreDeck { if (!isEnabled) ImGui::BeginDisabled(); StyleColor sc; - sc.push(ImGuiCol_Button, HexColor("#26B333", 0.10f)); - sc.push(ImGuiCol_ButtonHovered, HexColor("#26B333", 0.20f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#26B333", 0.30f)); - sc.push(ImGuiCol_Text, HexColor("#33CC47")); - sc.push(ImGuiCol_Border, HexColor("#33CC47")); + sc.push(ImGuiCol_Button, HexColor(Colors::PositiveFill, 0.10f)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::PositiveFill, 0.20f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::PositiveFill, 0.30f)); + sc.push(ImGuiCol_Text, HexColor(Colors::Positive)); + sc.push(ImGuiCol_Border, HexColor(Colors::Positive)); const bool clicked = ImGui::Button(label, size); if (!isEnabled) ImGui::EndDisabled(); @@ -92,16 +92,28 @@ namespace CoreDeck { return PrimaryButton(label, isEnabled, size); } + bool ToggleButton(const char *label, bool &isToggled, const ImVec2 size) { + StyleColor sc; + if (isToggled) { + sc.push(ImGuiCol_Button, HexColor(Colors::White, 0.10f)); + sc.push(ImGuiCol_Border, HexColor(Colors::White, 0.75f)); + sc.push(ImGuiCol_Text, HexColor(Colors::White)); + } + const bool clicked = ImGui::Button(label, size); + if (clicked) isToggled = !isToggled; + return clicked; + } + void StatusBadge(const char *label, const bool isActive) { StyleColor sc; StyleVar sv; if (isActive) { - sc.push(ImGuiCol_Button, HexColor("#26B333", 0.10f)); - sc.push(ImGuiCol_Text, HexColor("#33CC47")); + sc.push(ImGuiCol_Button, HexColor(Colors::PositiveFill, 0.10f)); + sc.push(ImGuiCol_Text, HexColor(Colors::Positive)); } else { - sc.push(ImGuiCol_Button, HexColor("#CC261F", 0.10f)); - sc.push(ImGuiCol_Text, HexColor("#E64D40")); + sc.push(ImGuiCol_Button, HexColor(Colors::NegativeStrong, 0.10f)); + sc.push(ImGuiCol_Text, HexColor(Colors::Negative)); } sc.push(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]); sc.push(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_Button]); @@ -124,12 +136,12 @@ namespace CoreDeck { StyleColor sc; StyleVar sv; - if (isSelected) sc.push(ImGuiCol_Button, HexColor("#29292B", 0.4f)); - else sc.push(ImGuiCol_Button, HexColor("#000000", 0.0f)); + if (isSelected) sc.push(ImGuiCol_Button, HexColor(Colors::Surface3, 0.4f)); + else sc.push(ImGuiCol_Button, HexColor(Colors::Shadow, 0.0f)); - sc.push(ImGuiCol_ButtonHovered, HexColor("#29292B", 0.4f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#29292B", 0.8f)); - sc.push(ImGuiCol_Text, HexColor("#F2F2F2")); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::Surface3, 0.4f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::Surface3, 0.8f)); + sc.push(ImGuiCol_Text, HexColor(Colors::TextPrimary)); sv.push(ImGuiStyleVar_FrameRounding, 6.0f); sv.push(ImGuiStyleVar_FrameBorderSize, 0.0f); @@ -210,8 +222,8 @@ namespace CoreDeck { const bool hovered = ImGui::IsItemHovered(); const ImU32 color = hovered - ? ImGui::ColorConvertFloat4ToU32(HexColor("#7AB8FF")) - : ImGui::ColorConvertFloat4ToU32(HexColor("#4D9AFF")); + ? ImGui::ColorConvertFloat4ToU32(HexColor(Colors::AccentInfoSoft)) + : ImGui::ColorConvertFloat4ToU32(HexColor(Colors::AccentInfo)); ImGui::GetWindowDrawList()->AddText(textPos, color, value); @@ -230,14 +242,14 @@ namespace CoreDeck { void PropertyTextWrapped(const char *label, const char *value, const bool invertColors) { StyleColor sc; - if (invertColors) sc.push(ImGuiCol_Text, HexColor("#66666B")); + if (invertColors) sc.push(ImGuiCol_Text, HexColor(Colors::TextMuted)); ImGui::Text("%s", label); ImGui::SameLine(); if (invertColors) { - sc.push(ImGuiCol_Text, HexColor("#F2F2F2")); + sc.push(ImGuiCol_Text, HexColor(Colors::TextPrimary)); } else { - sc.push(ImGuiCol_Text, HexColor("#66666B")); + sc.push(ImGuiCol_Text, HexColor(Colors::TextMuted)); } ImGui::TextWrapped("%s", value); } @@ -247,17 +259,17 @@ namespace CoreDeck { StyleVar sv; if (isSelected) { - sc.push(ImGuiCol_Button, HexColor("#26B333", 0.16f)); - sc.push(ImGuiCol_ButtonHovered, HexColor("#26B333", 0.24f)); - sc.push(ImGuiCol_ButtonActive, HexColor("#26B333", 0.32f)); - sc.push(ImGuiCol_Text, HexColor("#33CC47")); - sc.push(ImGuiCol_Border, HexColor("#33CC47")); + sc.push(ImGuiCol_Button, HexColor(Colors::PositiveFill, 0.16f)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::PositiveFill, 0.24f)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::PositiveFill, 0.32f)); + sc.push(ImGuiCol_Text, HexColor(Colors::Positive)); + sc.push(ImGuiCol_Border, HexColor(Colors::Positive)); } else { - sc.push(ImGuiCol_Button, HexColor("#1A1A1C")); - sc.push(ImGuiCol_ButtonHovered, HexColor("#242426")); - sc.push(ImGuiCol_ButtonActive, HexColor("#2E2E30")); - sc.push(ImGuiCol_Text, HexColor("#CFCFD4")); - sc.push(ImGuiCol_Border, HexColor("#2E2E33")); + sc.push(ImGuiCol_Button, HexColor(Colors::Surface2)); + sc.push(ImGuiCol_ButtonHovered, HexColor(Colors::Surface3)); + sc.push(ImGuiCol_ButtonActive, HexColor(Colors::Surface4)); + sc.push(ImGuiCol_Text, HexColor(Colors::TextHint)); + sc.push(ImGuiCol_Border, HexColor(Colors::Surface4)); } sv.push(ImGuiStyleVar_FrameRounding, 999.0f); @@ -269,12 +281,12 @@ namespace CoreDeck { bool CollapsingHeader(const char *label, const ImGuiTreeNodeFlags flags) { StyleColor sc; - sc.push(ImGuiCol_Header, HexColor("#000000", 0.0f)); - sc.push(ImGuiCol_HeaderHovered, HexColor("#000000", 0.0f)); - sc.push(ImGuiCol_HeaderActive, HexColor("#000000", 0.0f)); - sc.push(ImGuiCol_Border, HexColor("#000000", 0.0f)); - sc.push(ImGuiCol_BorderShadow, HexColor("#000000", 0.0f)); - sc.push(ImGuiCol_Text, HexColor("#969696")); + sc.push(ImGuiCol_Header, HexColor(Colors::Shadow, 0.0f)); + sc.push(ImGuiCol_HeaderHovered, HexColor(Colors::Shadow, 0.0f)); + sc.push(ImGuiCol_HeaderActive, HexColor(Colors::Shadow, 0.0f)); + sc.push(ImGuiCol_Border, HexColor(Colors::Shadow, 0.0f)); + sc.push(ImGuiCol_BorderShadow, HexColor(Colors::Shadow, 0.0f)); + sc.push(ImGuiCol_Text, HexColor(Colors::TextOnBright)); return ImGui::CollapsingHeader(label, flags); } @@ -303,7 +315,7 @@ namespace CoreDeck { return result; } - ImGui::PushStyleColor(ImGuiCol_Text, HexColor("#66666B")); + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextMuted)); ImGui::TextWrapped("%s", data.message); ImGui::PopStyleColor(); ImGui::Spacing(); diff --git a/src/gui/widgets.h b/src/gui/widgets.h index 02236a1..b2e2050 100644 --- a/src/gui/widgets.h +++ b/src/gui/widgets.h @@ -109,6 +109,8 @@ namespace CoreDeck { bool PickerButton(const char *label, bool isEnabled = true, ImVec2 size = ImVec2(0, 0)); + bool ToggleButton(const char *label, bool &isToggled, ImVec2 size = ImVec2(0, 0)); + void StatusBadge(const char *label, bool isActive); bool SelectableItem( diff --git a/src/gui/windows/about.cpp b/src/gui/windows/about.cpp index ad1e59d..aca060d 100644 --- a/src/gui/windows/about.cpp +++ b/src/gui/windows/about.cpp @@ -24,13 +24,13 @@ namespace CoreDeck { ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); const float titleWidth = ImGui::CalcTextSize(COREDECK_TITLE).x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - titleWidth) * 0.5f); - ImGui::TextColored(HexColor("#F2F2F2"), COREDECK_TITLE); + ImGui::TextColored(HexColor(Colors::TextPrimary), COREDECK_TITLE); ImGui::PopFont(); - const std::string versionText = "Version " COREDECK_VERSION " (Build " COREDECK_BUILD_NUMBER ")"; - const float versionWidth = ImGui::CalcTextSize(versionText.c_str()).x; + const std::string version = "Version " COREDECK_VERSION " (Build " COREDECK_BUILD_NUMBER ")"; + const float versionWidth = ImGui::CalcTextSize(version.c_str()).x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - versionWidth) * 0.5f); - ImGui::TextColored(HexColor("#66666B"), "%s", versionText.c_str()); + ImGui::TextColored(HexColor(Colors::TextMuted), "%s", version.c_str()); ImGui::Spacing(); ImGui::Separator(); @@ -62,7 +62,7 @@ namespace CoreDeck { const float copyrightWidth = ImGui::CalcTextSize(COREDECK_COPYRIGHT).x; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - copyrightWidth) * 0.5f); - ImGui::TextColored(HexColor("#66666B"), "%s", COREDECK_COPYRIGHT); + ImGui::TextColored(HexColor(Colors::TextMuted), "%s", COREDECK_COPYRIGHT); ImGui::Spacing(); ImGui::EndPopup(); diff --git a/src/gui/windows/avd_info.cpp b/src/gui/windows/avd_info.cpp index 44d6e17..669bbbf 100644 --- a/src/gui/windows/avd_info.cpp +++ b/src/gui/windows/avd_info.cpp @@ -52,6 +52,7 @@ namespace CoreDeck { const auto &GpuMode = avd.GpuMode; const auto &Arch = avd.Arch; const auto &Path = avd.Path; + const auto &SkinName = avd.SkinName; const auto args = BuildArgs(Name, GetDefaultAvdOptions(context)); std::string preview = context.Host.Sdk.EmulatorPath; @@ -75,6 +76,7 @@ namespace CoreDeck { if (!ScreenResolution.empty()) PropertyText("Resolution", ScreenResolution.c_str(), false, true); if (!SdCard.empty()) PropertyText("Storage", SdCard.c_str(), false, true); if (!GpuMode.empty()) PropertyText("GPU Mode", GpuModeDisplayLabel(GpuMode), false, true); + PropertyText("Skin", SkinName.empty() ? "None" : SkinName.c_str(), false, true); if (!avd.SystemImagePath.empty() || !avd.SystemImageVariant.empty() || @@ -88,7 +90,9 @@ namespace CoreDeck { PropertyText("16 KB Page Size", avd.Supports16KbPageSize ? "Supported" : "Not supported", false, true); if (!avd.SystemImageTagDisplayNames.empty()) { const std::string tags = JoinAvdInfoList(avd.SystemImageTagDisplayNames); - PropertyTextWrapped("Tags", tags.c_str(), true); + ImGui::Spacing(); + ImGui::TextDisabled("Tags"); + ImGui::TextWrapped("%s", tags.c_str()); } } diff --git a/src/gui/windows/avd_list.cpp b/src/gui/windows/avd_list.cpp index 711b337..a4c07b3 100644 --- a/src/gui/windows/avd_list.cpp +++ b/src/gui/windows/avd_list.cpp @@ -9,7 +9,6 @@ #include "../application.h" #include "../widgets.h" #include "../theme.h" -#include "../icons.h" namespace CoreDeck { struct DeviceIconStyle { @@ -22,12 +21,12 @@ namespace CoreDeck { d.reserve(device.size()); for (const char c: device) d.push_back(static_cast(std::tolower(static_cast(c)))); - if (d.find("wear") != std::string::npos) return {Icons::Watch, "#F5A623"}; - if (d.find("auto") != std::string::npos) return {Icons::Car, "#E64D40"}; - if (d.find("tv") != std::string::npos) return {Icons::Tv, "#7E57C2"}; + if (d.find("wear") != std::string::npos) return {Icons::Watch, Colors::AccentWear}; + if (d.find("auto") != std::string::npos) return {Icons::Car, Colors::Negative}; + if (d.find("tv") != std::string::npos) return {Icons::Tv, Colors::AccentTv}; if (d.find("tablet") != std::string::npos || d.find("pixel_c") != std::string::npos) - return {Icons::Tablet, "#33CC47"}; - return {Icons::Mobile, "#4FC3F7"}; + return {Icons::Tablet, Colors::AccentTablet}; + return {Icons::Mobile, Colors::AccentPhone}; } static const char *AvdTypeLabel(const AvdInfo &avd) { @@ -114,6 +113,11 @@ namespace CoreDeck { context.AvdCreationWork.SelectedGpuMode = 0; context.AvdCreationWork.NameAutoFilled = true; context.AvdCreationWork.DisplayNameAutoFilled = true; + context.AvdCreationWork.SkinAutoFilled = true; + context.AvdCreationWork.SelectedSkin = 0; + context.AvdCreationWork.PendingSelectedSkin = 0; + context.AvdCreationWork.LastDeviceForSkinAuto = -1; + context.AvdCreationWork.SkinSearchFilter[0] = '\0'; context.AvdCreationWork.Prefetch.Ready = false; context.AvdCreationWork.Prefetch.Loading = true; context.UI.ShowCreateAvdDialog = true; @@ -121,8 +125,10 @@ namespace CoreDeck { context.AvdCreationWork.Prefetch.Future = std::async(std::launch::async, [&context] { auto images = ListSystemImages(context.Host.Sdk); auto devices = ListDeviceProfiles(context.Host.Sdk); + auto skins = ListSkins(context.Host.Sdk); context.AvdCreationWork.SystemImages = std::move(images); context.AvdCreationWork.DeviceProfiles = std::move(devices); + context.AvdCreationWork.Skins = std::move(skins); context.AvdCreationWork.Prefetch.Loading = false; context.AvdCreationWork.Prefetch.Ready = true; }); @@ -246,7 +252,7 @@ namespace CoreDeck { ImGui::PushID(i); const char *avdStatusText = isRunning ? "Running..." : "Ready"; - const ImVec4 avdStatusColor = isRunning ? HexColor("#33CC47") : HexColor("#66666B"); + const ImVec4 avdStatusColor = isRunning ? HexColor(Colors::Positive) : HexColor(Colors::TextMuted); const std::string avdRightText = StrConcat(AvdTypeLabel(avd), " - ", avdStatusText); const auto [Icon, Color] = DeviceIconStyleFor(avd.Device); if (SelectableItem(avd.DisplayName.c_str(), isSelected, avdRightText.c_str(), avdStatusColor, Icon, HexColor(Color))) { diff --git a/src/gui/windows/avd_logs.cpp b/src/gui/windows/avd_logs.cpp index 9b0054c..8e09f30 100644 --- a/src/gui/windows/avd_logs.cpp +++ b/src/gui/windows/avd_logs.cpp @@ -3,109 +3,305 @@ // #include +#include +#include +#include +#include + #include "imgui.h" +#include "imgui_internal.h" #include "avd_logs.h" #include "../context.h" -#include "../icons.h" +#include "../theme.h" #include "../widgets.h" +#include "../../core/log_filter.h" namespace CoreDeck { - void BuildAvdLogsWindow(Context &context) { - if (!context.UI.ShowLogPanel) return; - - ImGui::Begin("Output Log"); + namespace { + struct PanelInputs { + std::shared_ptr Log; + std::string AvdName; + bool HasSelection = false; + }; - std::shared_ptr log = nullptr; - if (context.Catalog.SelectedAvd >= 0) { - log = context.Host.Manager.GetLog(context.Catalog.Avds[context.Catalog.SelectedAvd].Name); - } + struct PanelView { + LogFilterResult Filter; + std::string Placeholder; + bool HasContent = false; + }; - if (!log) ImGui::BeginDisabled(); - if (PrimaryButton(IconWithLabel(Icons::Trash, "Clear").c_str())) log->Clear(); - ImGui::SameLine(); - if (!log) ImGui::EndDisabled(); + struct SyncSelection { + bool Active = false; + int Start = 0; + int End = 0; + }; - constexpr float searchWidth = 300.0f; - const float windowWidth = ImGui::GetWindowWidth(); - constexpr float rightPadding = 8.0f; - ImGui::SetCursorPosX(windowWidth - searchWidth - rightPadding); - ImGui::SetNextItemWidth(searchWidth); - const std::string searchHint = std::string{Icons::Search} + " Search logs..."; + PanelInputs ResolveInputs(Context &context) { + PanelInputs inputs; + if (context.Catalog.SelectedAvd >= 0) { + inputs.HasSelection = true; + inputs.AvdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; + inputs.Log = context.Host.Manager.GetLog(inputs.AvdName); + } + return inputs; + } - std::string currentSearch; - if (context.Catalog.SelectedAvd >= 0) { - const std::string &avdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; - currentSearch = context.Logs.PerAvdLogSearch[avdName]; + Context::LogViewState &ResolveViewState(Context &context, const std::string &avdName) { + return context.Logs.PerAvdView[avdName]; } - char searchBuffer[256]; - strncpy(searchBuffer, currentSearch.c_str(), sizeof(searchBuffer) - 1); - searchBuffer[sizeof(searchBuffer) - 1] = '\0'; + PanelView BuildView(const PanelInputs &inputs, const Context::LogViewState &state) { + PanelView view; + if (!inputs.HasSelection) { + view.Placeholder = "Select an AVD to view logs"; + return view; + } + if (!inputs.Log) { + view.Placeholder = "Run the \"" + inputs.AvdName + "\" AVD to view logs"; + return view; + } - if (ImGui::InputTextWithHint("##search", searchHint.c_str(), searchBuffer, sizeof(searchBuffer))) { - if (context.Catalog.SelectedAvd >= 0) { - const std::string &avdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; - context.Logs.PerAvdLogSearch[avdName] = searchBuffer; + const auto lines = inputs.Log->GetLines(); + LogFilterOptions options; + options.Query = state.Search; + options.UseRegex = state.UseRegex; + view.Filter = FilterLog(lines, options); + view.HasContent = !view.Filter.Joined.empty(); + + if (!view.HasContent) { + view.Placeholder = lines.empty() ? "No available logs to view" : "No matching log entries found"; } + return view; } - ImGui::Separator(); - ImGui::BeginChild("LogContent", ImVec2(0, 0), ImGuiChildFlags_None); + bool RenderToolbarButtons(const PanelInputs &inputs, const bool hasContent) { + const bool disabled = !inputs.Log; + if (disabled) ImGui::BeginDisabled(); + if (PrimaryButton(IconWithLabel(Icons::Trash, "Clear").c_str())) { + inputs.Log->Clear(); + } + if (disabled) ImGui::EndDisabled(); + ImGui::SameLine(); - if (context.Catalog.SelectedAvd < 0) { - ImGui::TextDisabled("Select an AVD to view logs"); - ImGui::EndChild(); - ImGui::End(); - return; + const bool canCopy = inputs.Log && hasContent; + if (!canCopy) ImGui::BeginDisabled(); + const bool copyClicked = PrimaryButton(IconWithLabel(Icons::Copy, "Copy").c_str()); + if (!canCopy) ImGui::EndDisabled(); + ImGui::SameLine(); + return copyClicked; } - if (log) { - const auto lines = log->GetLines(); + bool RenderSearchBar(Context::LogViewState &state, const PanelView &view, const int matchCount, bool &queryChanged) { + queryChanged = false; + bool navChanged = false; + + constexpr float regexToggleWidth = 32.0f; + constexpr float navButtonWidth = 32.0f; + constexpr float searchWidth = 260.0f; + + const bool hasQueryForWidth = !state.Search.empty(); + const bool regexInvalidForWidth = state.UseRegex && hasQueryForWidth && !view.Filter.RegexValid; + const int displayedIndexForWidth = matchCount > 0 ? state.ActiveMatchIndex + 1 : 0; + std::string counter; + if (!hasQueryForWidth) counter = "0 / 0"; + else if (regexInvalidForWidth) counter = "—"; + else counter = std::to_string(displayedIndexForWidth) + " / " + std::to_string(matchCount); - if (lines.empty()) { - ImGui::TextDisabled("No available logs to view"); - ImGui::EndChild(); - ImGui::End(); - return; + const ImGuiStyle &style = ImGui::GetStyle(); + const float counterWidth = ImGui::CalcTextSize(counter.c_str()).x; + const float totalWidth = regexToggleWidth + searchWidth + counterWidth + navButtonWidth * 2.0f + style.ItemSpacing.x * 4.0f; + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - totalWidth); + + // Regex toggle + if (ToggleButton(".*##RegexToggle", state.UseRegex, ImVec2(regexToggleWidth, 0))) { + queryChanged = true; } + ImGui::SameLine(); + + // Search field + const bool regexInvalid = state.UseRegex && !state.Search.empty() && !view.Filter.RegexValid; + char searchBuffer[256]; + std::strncpy(searchBuffer, state.Search.c_str(), sizeof(searchBuffer) - 1); + searchBuffer[sizeof(searchBuffer) - 1] = '\0'; - const std::string &avdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; - const std::string searchQuery = context.Logs.PerAvdLogSearch[avdName]; - const bool hasSearch = !searchQuery.empty(); - - bool hasVisibleLines = false; - for (const auto &line: lines) { - // Filter lines based on search query (case-insensitive) - if (hasSearch) { - std::string lowerLine = line; - std::string lowerQuery = searchQuery; - std::ranges::transform(lowerLine, lowerLine.begin(), tolower); - std::ranges::transform(lowerQuery, lowerQuery.begin(), tolower); - - if (lowerLine.find(lowerQuery) == std::string::npos) { - continue; - } - } - - ImGui::TextUnformatted(line.c_str()); - hasVisibleLines = true; + const std::string hint = IconWithLabel(Icons::Search, state.UseRegex ? "Regex" : "Search logs..."); + ImGui::SetNextItemWidth(searchWidth); + if (regexInvalid) ImGui::PushStyleColor(ImGuiCol_Border, HexColor(Colors::Negative)); + if (regexInvalid) ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + const bool edited = ImGui::InputTextWithHint("##search", hint.c_str(), searchBuffer, sizeof(searchBuffer)); + const bool enterPressed = ImGui::IsItemDeactivatedAfterEdit() && ImGui::IsKeyPressed(ImGuiKey_Enter, false); + if (regexInvalid) ImGui::PopStyleVar(); + if (regexInvalid) ImGui::PopStyleColor(); + if (regexInvalid && ImGui::IsItemHovered()) { + ImGui::SetTooltip("Invalid regex: %s", view.Filter.RegexError.c_str()); } + if (edited) { + state.Search = searchBuffer; + queryChanged = true; + } + ImGui::SameLine(); + + // Match counter + ImGui::TextDisabled("%s", counter.c_str()); + ImGui::SameLine(); - if (hasSearch && !hasVisibleLines) { - ImGui::TextDisabled("No matching log entries found"); + // Prev / Next + const bool canNav = matchCount > 0; + if (!canNav) ImGui::BeginDisabled(); + if (ImGui::Button((std::string{Icons::ChevronLeft} + "##LogPrev").c_str(), ImVec2(navButtonWidth, 0))) { + state.ActiveMatchIndex = (state.ActiveMatchIndex - 1 + matchCount) % matchCount; + navChanged = true; } + ImGui::SameLine(); + if (ImGui::Button((std::string{Icons::ChevronRight} + "##LogNext").c_str(), ImVec2(navButtonWidth, 0))) { + state.ActiveMatchIndex = (state.ActiveMatchIndex + 1) % matchCount; + navChanged = true; + } + if (!canNav) ImGui::EndDisabled(); - if (context.Logs.AutoScroll && log->HasNewContent() && !hasSearch) { - ImGui::SetScrollHereY(1.0f); - log->ResetNewContentFlag(); + if (canNav && enterPressed) { + state.ActiveMatchIndex = (state.ActiveMatchIndex + 1) % matchCount; + navChanged = true; } - } else { - const std::string &avdName = context.Catalog.Avds[context.Catalog.SelectedAvd].Name; - ImGui::TextDisabled("Run the \"%s\" AVD to view logs", avdName.c_str()); + + return navChanged; + } + + int CallbackSetSelection(ImGuiInputTextCallbackData *data) { + const auto *selection = static_cast(data->UserData); + if (selection && selection->Active) { + data->CursorPos = selection->End; + data->SelectionStart = selection->Start; + data->SelectionEnd = selection->End; + } + return 0; + } + + std::size_t LineIndexFor(const std::string &joined, const std::size_t offset) { + std::size_t line = 0; + const std::size_t end = std::min(offset, joined.size()); + for (std::size_t i = 0; i < end; ++i) { + if (joined[i] == '\n') ++line; + } + return line; + } + + ImGuiWindow *GetLogChildWindow() { + return ImGui::FindWindowByID(ImGui::GetID("##LogText")); + } + + bool ApplyScrollToLine(const int lineIndex) { + if (lineIndex < 0) return false; + ImGuiWindow *window = GetLogChildWindow(); + if (!window) return false; + const float lineHeight = ImGui::GetTextLineHeight(); + const float regionH = window->InnerRect.GetHeight(); + const float targetY = static_cast(lineIndex) * lineHeight; + window->Scroll.y = std::max(0.0f, targetY - regionH * 0.3f); + return true; + } + + bool ApplyScrollToBottom() { + ImGuiWindow *w = GetLogChildWindow(); + if (!w) return false; + w->Scroll.y = w->ScrollMax.y; + return true; + } + + void RenderLogBody(const PanelView &view, const SyncSelection &sync, const bool focusLog) { + const std::string &display = view.HasContent ? view.Filter.Joined : view.Placeholder; + std::vector buffer(display.begin(), display.end()); + buffer.push_back('\0'); + + ImGuiInputTextFlags flags = ImGuiInputTextFlags_ReadOnly | + ImGuiInputTextFlags_NoUndoRedo; + if (sync.Active) flags |= ImGuiInputTextFlags_CallbackAlways; + + if (!view.HasContent) ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, HexColor(Colors::AccentInfo, 0.55f)); + if (focusLog) ImGui::SetKeyboardFocusHere(); + ImGui::InputTextMultiline( + "##LogText", + buffer.data(), + buffer.size(), + ImVec2(-FLT_MIN, -FLT_MIN), + flags, + sync.Active ? CallbackSetSelection : nullptr, + sync.Active ? const_cast(&sync) : nullptr + ); + ImGui::PopStyleColor(); + if (!view.HasContent) ImGui::PopStyleColor(); + } + + void DriveAutoScroll(const PanelInputs &inputs, const Context &context, const PanelView &view, const bool hasQuery) { + if (!inputs.Log || !context.Logs.AutoScroll || hasQuery || !view.HasContent) return; + if (!inputs.Log->HasNewContent()) return; + + ApplyScrollToBottom(); + inputs.Log->ResetNewContentFlag(); } + } + + void BuildAvdLogsWindow(Context &context) { + if (!context.UI.ShowLogPanel) return; + + ImGui::Begin("Output Log"); + + const PanelInputs inputs = ResolveInputs(context); + Context::LogViewState scratch{}; + Context::LogViewState &state = inputs.HasSelection ? ResolveViewState(context, inputs.AvdName) : scratch; + + PanelView view = BuildView(inputs, state); + const int matchCount = static_cast(view.Filter.Matches.size()); + + if (state.ActiveMatchIndex >= matchCount) state.ActiveMatchIndex = 0; + if (state.ActiveMatchIndex < 0) state.ActiveMatchIndex = 0; + + const bool copyClicked = RenderToolbarButtons(inputs, view.HasContent); + bool queryChanged = false; + const bool navChanged = RenderSearchBar(state, view, matchCount, queryChanged); + + if (queryChanged) { + state.ActiveMatchIndex = 0; + view = BuildView(inputs, state); + context.Logs.PendingScroll = !view.Filter.Matches.empty(); + if (!view.Filter.Matches.empty()) context.Logs.PendingSyncFrames = 2; + } + if (navChanged) { + context.Logs.PendingScroll = true; + context.Logs.PendingFocus = true; + context.Logs.PendingSyncFrames = 2; + } + + if (copyClicked && view.HasContent) ImGui::SetClipboardText(view.Filter.Joined.c_str()); + + SyncSelection sync; + int scrollLine = -1; + const bool haveActiveMatch = !view.Filter.Matches.empty() && state.ActiveMatchIndex < static_cast(view.Filter.Matches.size()); + if (haveActiveMatch && context.Logs.PendingSyncFrames > 0) { + const auto &[StartOffset, EndOffset] = view.Filter.Matches[state.ActiveMatchIndex]; + sync.Active = true; + sync.Start = static_cast(StartOffset); + sync.End = static_cast(EndOffset); + } + if (haveActiveMatch && context.Logs.PendingScroll) { + const auto &[StartOffset, _] = view.Filter.Matches[state.ActiveMatchIndex]; + scrollLine = static_cast(LineIndexFor(view.Filter.Joined, StartOffset)); + } + + const bool focusLog = context.Logs.PendingFocus && haveActiveMatch; + + bool scrollApplied = true; + if (scrollLine >= 0) scrollApplied = ApplyScrollToLine(scrollLine); + if (scrollApplied) context.Logs.PendingScroll = false; + + context.Logs.PendingFocus = false; + if (context.Logs.PendingSyncFrames > 0) --context.Logs.PendingSyncFrames; + + RenderLogBody(view, sync, focusLog); + if (scrollLine < 0) DriveAutoScroll(inputs, context, view, !state.Search.empty()); - ImGui::EndChild(); ImGui::End(); } -} +} \ No newline at end of file diff --git a/src/gui/windows/avd_options.cpp b/src/gui/windows/avd_options.cpp index c0c12a7..ed205a8 100644 --- a/src/gui/windows/avd_options.cpp +++ b/src/gui/windows/avd_options.cpp @@ -8,7 +8,6 @@ #include "../application.h" #include "../widgets.h" #include "../theme.h" -#include "../icons.h" namespace CoreDeck { void BuildAvdOptionsWindow(Context &context) { @@ -53,7 +52,7 @@ namespace CoreDeck { if (wasEnabled != Enabled) optionsChanged = true; ImGui::SameLine(); - ImGui::TextColored(HexColor("#66666B"), Icons::Info); + ImGui::TextColored(HexColor(Colors::TextMuted), Icons::Info); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Description.c_str()); if (Enabled) { diff --git a/src/gui/windows/create_avd.cpp b/src/gui/windows/create_avd.cpp index ed0b7b0..f41c8ee 100644 --- a/src/gui/windows/create_avd.cpp +++ b/src/gui/windows/create_avd.cpp @@ -8,6 +8,7 @@ #include "create_avd.h" #include "device_profile.h" #include "install_image.h" +#include "skin.h" #include "../application.h" #include "../widgets.h" #include "../theme.h" @@ -107,7 +108,7 @@ namespace CoreDeck { if (nameConflict) { ImGui::TextColored( - HexColor("#E64D40"), + HexColor(Colors::Negative), " An AVD named \"%s\" already exists.", context.AvdCreationWork.CreationData.Name.c_str() ); @@ -135,7 +136,7 @@ namespace CoreDeck { } else { PickerButton("No system images installed", false, ImVec2(-1.0f, 0.0f)); ImGui::TextColored( - HexColor("#E64D40"), + HexColor(Colors::Negative), "SDK Manager was not found, so CoreDeck cannot install images automatically." ); } @@ -173,6 +174,38 @@ namespace CoreDeck { PickerButton("Loading device profiles...", false, ImVec2(-1.0f, 0.0f)); } + if (context.AvdCreationWork.Prefetch.Ready && hasDeviceProfile) { + auto &skinWork = context.AvdCreationWork; + if (skinWork.SkinAutoFilled && skinWork.SelectedDevice != skinWork.LastDeviceForSkinAuto) { + const auto &deviceId = skinWork.DeviceProfiles[skinWork.SelectedDevice].Id; + const auto match = FindSkinForDevice(skinWork.Skins, deviceId); + if (match.has_value()) { + for (int i = 0; i < static_cast(skinWork.Skins.size()); i++) { + if (skinWork.Skins[i].Name == match->Name) { + skinWork.SelectedSkin = i + 1; + break; + } + } + } else { + skinWork.SelectedSkin = 0; + } + skinWork.LastDeviceForSkinAuto = skinWork.SelectedDevice; + } + } + + ImGui::Spacing(); + ImGui::Text("Skin"); + if (context.AvdCreationWork.Prefetch.Ready) { + const std::string skinPreview = SkinPreviewLabel(context); + if (PickerButton(skinPreview.c_str(), !formDisabled, ImVec2(-1.0f, 0.0f))) { + context.AvdCreationWork.PendingSelectedSkin = context.AvdCreationWork.SelectedSkin; + context.AvdCreationWork.SkinSearchFilter[0] = '\0'; + context.UI.ShowSkinDialog = true; + } + } else { + PickerButton("Loading skins...", false, ImVec2(-1.0f, 0.0f)); + } + ImGui::Spacing(); const float rowSpacing = ImGui::GetStyle().ItemSpacing.x; const float colWidth = (ImGui::GetContentRegionAvail().x - rowSpacing) * 0.5f; @@ -241,6 +274,15 @@ namespace CoreDeck { ? context.AvdCreationWork.DeviceProfiles[context.AvdCreationWork.SelectedDevice].Id : ""; context.AvdCreationWork.CreationData.GpuMode = gpuModes[context.AvdCreationWork.SelectedGpuMode].Value; + if (context.AvdCreationWork.SelectedSkin > 0 && + context.AvdCreationWork.SelectedSkin - 1 < static_cast(context.AvdCreationWork.Skins.size())) { + const auto &chosenSkin = context.AvdCreationWork.Skins[context.AvdCreationWork.SelectedSkin - 1]; + context.AvdCreationWork.CreationData.SkinName = chosenSkin.Name; + context.AvdCreationWork.CreationData.SkinPath = chosenSkin.Path; + } else { + context.AvdCreationWork.CreationData.SkinName.clear(); + context.AvdCreationWork.CreationData.SkinPath.clear(); + } if (!context.AvdCreationWork.CreationData.SdCardSize.empty()) { context.AvdCreationWork.CreationData.SdCardSize += "M"; } @@ -266,6 +308,7 @@ namespace CoreDeck { } BuildDeviceProfileWindow(context); + BuildSkinWindow(context); BuildInstallImageWindow(context); ImGui::EndPopup(); diff --git a/src/gui/windows/device_profile.cpp b/src/gui/windows/device_profile.cpp index 2b072b3..28f3085 100644 --- a/src/gui/windows/device_profile.cpp +++ b/src/gui/windows/device_profile.cpp @@ -7,7 +7,6 @@ #include "imgui.h" #include "device_profile.h" -#include "../icons.h" #include "../theme.h" #include "../widgets.h" #include "../../core/utilities.h" @@ -45,22 +44,22 @@ namespace CoreDeck { static LabeledIconStyle DeviceProfileStyleFor(const DeviceProfile &device) { switch (DeviceCategoryForProfile(device)) { case DeviceCategory::Phone: - return {Icons::Mobile, "Phone", "#4FC3F7"}; + return {Icons::Mobile, "Phone", Colors::AccentPhone}; case DeviceCategory::Tablet: - return {Icons::Tablet, "Tablet", "#33CC47"}; + return {Icons::Tablet, "Tablet", Colors::AccentTablet}; case DeviceCategory::Wear: - return {Icons::Watch, "Wear OS", "#F5A623"}; + return {Icons::Watch, "Wear OS", Colors::AccentWear}; case DeviceCategory::Tv: - return {Icons::Tv, "TV", "#7E57C2"}; + return {Icons::Tv, "TV", Colors::AccentTv}; case DeviceCategory::Automotive: - return {Icons::Car, "Automotive", "#E64D40"}; + return {Icons::Car, "Automotive", Colors::Negative}; case DeviceCategory::Desktop: - return {Icons::Desktop, "Desktop", "#A7A7AD"}; + return {Icons::Desktop, "Desktop", Colors::TextSubtle}; case DeviceCategory::All: case DeviceCategory::Other: - return {Icons::Gear, "Other", "#A7A7AD"}; + return {Icons::Gear, "Other", Colors::TextSubtle}; } - return {Icons::Gear, "Other", "#A7A7AD"}; + return {Icons::Gear, "Other", Colors::TextSubtle}; } std::string DeviceProfilePreviewLabel(const DeviceProfile &device) { diff --git a/src/gui/windows/install_image.cpp b/src/gui/windows/install_image.cpp index 41b6483..35d7aa4 100644 --- a/src/gui/windows/install_image.cpp +++ b/src/gui/windows/install_image.cpp @@ -9,7 +9,6 @@ #include "install_image.h" #include "../application.h" -#include "../icons.h" #include "../widgets.h" #include "../theme.h" #include "../../core/utilities.h" @@ -111,12 +110,12 @@ namespace CoreDeck { } LabeledIconStyle SystemImageTypeStyleForVariant(const std::string &variant) { - if (variant.starts_with("google_apis_playstore")) return {Icons::Play, "Google Play", "#33CC47"}; - if (variant.starts_with("google_apis")) return {Icons::Gear, "Google APIs", "#4FC3F7"}; + if (variant.starts_with("google_apis_playstore")) return {Icons::Play, "Google Play", Colors::Positive}; + if (variant.starts_with("google_apis")) return {Icons::Gear, "Google APIs", Colors::AccentPhone}; if (variant.starts_with("aosp_atd") || variant.starts_with("google_atd")) { - return {Icons::Mobile, "ATD", "#F5A623"}; + return {Icons::Mobile, "ATD", Colors::AccentWear}; } - return {Icons::Mobile, "Default", "#A7A7AD"}; + return {Icons::Mobile, "Default", Colors::TextSubtle}; } LabeledIconStyle SystemImageTypeStyleFor(const SystemImage &img) { @@ -303,7 +302,7 @@ namespace CoreDeck { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextColored( - HexColor("#E64D40"), + HexColor(Colors::Negative), "No remote system images found. Check your SDK and internet connection." ); } else { @@ -340,7 +339,7 @@ namespace CoreDeck { ImGui::TableNextColumn(); if (img.IsInstalled) { - ImGui::TextColored(HexColor("#33CC47"), "Installed"); + ImGui::TextColored(HexColor(Colors::Positive), "Installed"); } else { ImGui::TextDisabled("Available"); } @@ -376,7 +375,7 @@ namespace CoreDeck { ImGui::Text("%s", statusText.c_str()); ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, HexColor("#33CC47")); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, HexColor(Colors::Positive)); ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f)); ImGui::PopStyleColor(); } @@ -398,8 +397,8 @@ namespace CoreDeck { ImGui::SetCursorPosX( (ImGui::GetContentRegionAvail().x - textWidth) * 0.5f + ImGui::GetCursorStartPos().x ); - if (succeeded) ImGui::TextColored(HexColor("#33CC47"), "%s", statusText.c_str()); - else ImGui::TextColored(HexColor("#E64D40"), "%s", statusText.c_str()); + if (succeeded) ImGui::TextColored(HexColor(Colors::Positive), "%s", statusText.c_str()); + else ImGui::TextColored(HexColor(Colors::Negative), "%s", statusText.c_str()); } } @@ -424,7 +423,7 @@ namespace CoreDeck { const bool licenseBusy = work.LicenseBusy.load(); if (!work.LicenseError.empty()) { - ImGui::TextColored(HexColor("#E64D40"), "%s", work.LicenseError.c_str()); + ImGui::TextColored(HexColor(Colors::Negative), "%s", work.LicenseError.c_str()); ImGui::Spacing(); } diff --git a/src/gui/windows/main_menu_bar.cpp b/src/gui/windows/main_menu_bar.cpp index c713379..de1e9bc 100644 --- a/src/gui/windows/main_menu_bar.cpp +++ b/src/gui/windows/main_menu_bar.cpp @@ -7,7 +7,7 @@ #include "main_menu_bar.h" #include "../widgets.h" -#include "../icons.h" +#include "../theme.h" #include "../application.h" namespace CoreDeck { @@ -25,20 +25,20 @@ namespace CoreDeck { } if (ImGui::BeginMenu("View")) { - if (ImGui::MenuItem("AVD List", nullptr, &context.UI.ShowAvdListPanel)) { + if (ImGui::MenuItem("Show AVD List Window", nullptr, &context.UI.ShowAvdListPanel)) { PersistAppSettings(context); } - if (ImGui::MenuItem("Options", nullptr, &context.UI.ShowOptionsPanel)) { + if (ImGui::MenuItem("Show Options Window", nullptr, &context.UI.ShowOptionsPanel)) { PersistAppSettings(context); } - if (ImGui::MenuItem("Details", nullptr, &context.UI.ShowDetailsPanel)) { + if (ImGui::MenuItem("Show Details Window", nullptr, &context.UI.ShowDetailsPanel)) { PersistAppSettings(context); } - if (ImGui::MenuItem("Output Log", nullptr, &context.UI.ShowLogPanel)) { + if (ImGui::MenuItem("Show Output Log Window", nullptr, &context.UI.ShowLogPanel)) { PersistAppSettings(context); } ImGui::Separator(); - if (ImGui::MenuItem("Storage Overview...")) { + if (ImGui::MenuItem("Storage Overview")) { context.UI.ShowStorageDialog = true; } ImGui::EndMenu(); diff --git a/src/gui/windows/onboarding.cpp b/src/gui/windows/onboarding.cpp index 1f19b5e..1e677b6 100644 --- a/src/gui/windows/onboarding.cpp +++ b/src/gui/windows/onboarding.cpp @@ -34,11 +34,11 @@ namespace CoreDeck { VerticalCenter(260.0f); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - CenteredText(COREDECK_TITLE, HexColor("#F2F2F2")); + CenteredText(COREDECK_TITLE, HexColor(Colors::TextPrimary)); ImGui::PopFont(); ImGui::Spacing(); - CenteredText("Your Android emulator command center.", HexColor("#66666B")); + CenteredText("Your Android emulator command center.", HexColor(Colors::TextMuted)); ImGui::Spacing(); ImGui::Spacing(); @@ -46,8 +46,8 @@ namespace CoreDeck { const auto welcomeLine1 = "Welcome! CoreDeck helps you manage Android emulators"; const auto welcomeLine2 = "faster and cleaner than the default tooling."; - CenteredText(welcomeLine1, HexColor("#A8A8AD")); - CenteredText(welcomeLine2, HexColor("#A8A8AD")); + CenteredText(welcomeLine1, HexColor(Colors::TextSubtle)); + CenteredText(welcomeLine2, HexColor(Colors::TextSubtle)); ImGui::Spacing(); ImGui::Spacing(); @@ -65,12 +65,12 @@ namespace CoreDeck { VerticalCenter(320.0f); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); - CenteredText("Locate your Android SDK", HexColor("#F2F2F2")); + CenteredText("Locate your Android SDK", HexColor(Colors::TextPrimary)); ImGui::PopFont(); ImGui::Spacing(); - CenteredText("CoreDeck needs to know where your Android SDK lives.", HexColor("#66666B")); - CenteredText("This is where 'emulator', 'avdmanager' and system images are installed.", HexColor("#66666B")); + CenteredText("CoreDeck needs to know where your Android SDK lives.", HexColor(Colors::TextMuted)); + CenteredText("This is where 'emulator', 'avdmanager' and system images are installed.", HexColor(Colors::TextMuted)); ImGui::Spacing(); ImGui::Spacing(); @@ -98,20 +98,20 @@ namespace CoreDeck { if (!currentPath.empty()) { if (isValid) { ImGui::TextColored( - HexColor("#33CC47"), + HexColor(Colors::Positive), "%s", "Looks good. Found the Android emulator at this location." ); } else { ImGui::TextColored( - HexColor("#E64D40"), + HexColor(Colors::Negative), "%s", "Couldn't find the Android emulator here. Make sure this is your SDK root folder." ); } } else { ImGui::TextColored( - HexColor("#66666B"), + HexColor(Colors::TextMuted), "%s", "Choose the folder containing your Android SDK (cmdline-tools, emulator, platform-tools, etc)." ); diff --git a/src/gui/windows/preferences.cpp b/src/gui/windows/preferences.cpp index 41bfb2d..bbd4db0 100644 --- a/src/gui/windows/preferences.cpp +++ b/src/gui/windows/preferences.cpp @@ -3,6 +3,7 @@ // #include "imgui.h" +#include "imgui_internal.h" #include "preferences.h" #include "../widgets.h" @@ -13,73 +14,156 @@ #include "../../core/file_dialog.h" namespace CoreDeck { - void BuildPreferencesWindow(Context &context) { - if (context.UI.ShowPreferences && !ImGui::IsPopupOpen("Preferences###CoreDeckPrefs")) { - ImGui::OpenPopup("Preferences###CoreDeckPrefs"); - } + namespace { + enum class PrefsSection { + General, + AndroidSdk, + }; - const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(ImVec2(520, 0), ImGuiCond_Appearing); + struct SidebarItem { + PrefsSection Section; + const char *Icon; + const char *Label; + }; - static char sdkPathBuffer[2048]; + constexpr SidebarItem SidebarItems[] = { + {PrefsSection::General, Icons::Gear, "General"}, + {PrefsSection::AndroidSdk, Icons::Mobile, "Android SDK"}, + }; - if (ImGui::BeginPopupModal("Preferences###CoreDeckPrefs", &context.UI.ShowPreferences, WindowNoResizeFlags)) { - if (ImGui::IsWindowAppearing()) { - const std::string &p = context.Host.Sdk.SdkPath; - strncpy(sdkPathBuffer, p.c_str(), sizeof(sdkPathBuffer) - 1); - sdkPathBuffer[sizeof(sdkPathBuffer) - 1] = '\0'; + bool SidebarRow(const SidebarItem &item, const bool selected) { + ImGuiWindow *window = ImGui::GetCurrentWindow(); + const float width = ImGui::GetContentRegionAvail().x; + const float height = ImGui::GetFrameHeight() + 6.0f; + + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImRect bb(pos, ImVec2(pos.x + width, pos.y + height)); + + ImGui::PushID(item.Label); + const ImGuiID id = window->GetID(item.Label); + ImGui::ItemSize(ImVec2(width, height)); + if (!ImGui::ItemAdd(bb, id)) { + ImGui::PopID(); + return false; + } + + bool hovered, held; + const bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); + + ImU32 bg = 0; + if (selected) { + bg = ImGui::GetColorU32(HexColor(Colors::Surface3)); + } else if (hovered) { + bg = ImGui::GetColorU32(HexColor(Colors::Surface2)); + } + if (bg) { + window->DrawList->AddRectFilled(bb.Min, bb.Max, bg); } - ImGui::TextUnformatted("General"); + if (selected) { + const ImVec2 a(bb.Min.x, bb.Min.y); + const ImVec2 b(bb.Min.x + 4.0f, bb.Max.y); + window->DrawList->AddRectFilled(a, b, IM_COL32_WHITE); + } + + const ImU32 textColor = ImGui::GetColorU32(selected ? HexColor(Colors::TextPrimary) : HexColor(Colors::TextSubtle)); + const float textY = bb.Min.y + (height - ImGui::GetTextLineHeight()) * 0.5f; + window->DrawList->AddText(ImVec2(bb.Min.x + 14.0f, textY), textColor, item.Icon); + window->DrawList->AddText(ImVec2(bb.Min.x + 38.0f, textY), textColor, item.Label); + + ImGui::PopID(); + return pressed; + } + + void SectionHeader(const char *title, const char *subtitle) { + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextPrimary)); + ImGui::TextUnformatted(title); + ImGui::PopStyleColor(); + if (subtitle && *subtitle) { + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextSubtle)); + ImGui::TextWrapped("%s", subtitle); + ImGui::PopStyleColor(); + } + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); + } + + bool CheckboxRow(const char *id, const char *title, const char *tooltip, bool *value) { + ImGui::PushID(id); + const bool changed = ImGui::Checkbox(title, value); + if (tooltip && *tooltip) { + ImGui::SameLine(); + ImGui::TextColored(HexColor(Colors::TextMuted), Icons::Info); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip); + } + ImGui::Spacing(); + ImGui::PopID(); + return changed; + } - if (ImGui::Checkbox("Auto-scroll output log", &context.Logs.AutoScroll)) { + void DrawGeneralSection(Context &context) { + SectionHeader("General", "Behavior of CoreDeck while you work with AVDs."); + + if (CheckboxRow( + "autoscroll", + "Auto-scroll output log", + "Keep the log view pinned to the most recent line as new output arrives.", + &context.Logs.AutoScroll + )) { PersistAppSettings(context); } - if (ImGui::Checkbox("Confirm before deleting an AVD", &context.Prefs.ConfirmBeforeDeleteAvd)) { + + if (CheckboxRow( + "confirmdelete", + "Confirm before deleting an AVD", + "Show a confirmation dialog when you delete a virtual device.", + &context.Prefs.ConfirmBeforeDeleteAvd + )) { PersistAppSettings(context); } + } - ImGui::Spacing(); - ImGui::TextUnformatted("Android SDK"); - ImGui::Separator(); - ImGui::Spacing(); + void DrawAndroidSdkSection(Context &context, char *sdkPathBuffer, size_t bufferSize) { + SectionHeader("Android SDK", "Where CoreDeck looks for the emulator and command-line tools."); + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextPrimary)); ImGui::TextUnformatted("SDK root"); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 120.0f); - ImGui::InputTextWithHint("##sdk_pref", "Path to Android SDK", sdkPathBuffer, sizeof(sdkPathBuffer)); + ImGui::PopStyleColor(); + constexpr float browseW = 110.0f; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - browseW - spacing); + ImGui::InputTextWithHint("##SdkPrefs", "Path to Android SDK", sdkPathBuffer, bufferSize); ImGui::SameLine(); - if (PrimaryButton("Browse...", true, ImVec2(110, 0))) { - if (const auto picked = FileDialog::PickFolder("Select Android SDK folder", sdkPathBuffer)) { - strncpy(sdkPathBuffer, picked->c_str(), sizeof(sdkPathBuffer) - 1); - sdkPathBuffer[sizeof(sdkPathBuffer) - 1] = '\0'; + if (PrimaryButton("Browse...", true, ImVec2(browseW, 0))) { + if (const auto picked = FileDialog::PickFolder("Select Android SDK directory", sdkPathBuffer)) { + strncpy(sdkPathBuffer, picked->c_str(), bufferSize - 1); + sdkPathBuffer[bufferSize - 1] = '\0'; } } const std::string pathStr = sdkPathBuffer; const bool pathOk = Paths::Onboarding::ValidateSdkPath(pathStr); + if (!pathStr.empty()) { if (pathOk) { - ImGui::TextColored(HexColor("#33CC47"), "This folder looks like a valid Android SDK."); + ImGui::TextColored(HexColor(Colors::Positive), "Valid Android SDK path."); } else { ImGui::TextColored( - HexColor("#E64D40"), + HexColor(Colors::Negative), "Not a valid SDK (need emulator and cmdline-tools with avdmanager)." ); } + } else { + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextSubtle)); + ImGui::TextUnformatted("Leave empty to auto-detect from ANDROID_HOME or default install paths."); + ImGui::PopStyleColor(); } + ImGui::Spacing(); ImGui::Spacing(); - constexpr float applyW = 160.0f; - constexpr float defaultW = 180.0f; - const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float totalW = applyW + spacing + defaultW; - ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - totalW + ImGui::GetCursorPosX()); - - if (PrimaryButton("Apply SDK path", pathOk, ImVec2(applyW, 0))) { + if (PrimaryButton("Apply SDK Path", pathOk)) { Paths::Onboarding::SaveSdkPathOverride(pathStr); context.Host.Sdk = DetectAndroidSdk(); context.Host.Manager.SetSdk(context.Host.Sdk); @@ -92,34 +176,105 @@ namespace CoreDeck { } ImGui::SameLine(); - if (PrimaryButton("Use default discovery", true, ImVec2(defaultW, 0))) { + if (PrimaryButton("Use Default Discovery", true)) { Paths::Onboarding::ClearSdkPathOverride(); context.Host.Sdk = DetectAndroidSdk(); context.Host.Manager.SetSdk(context.Host.Sdk); RefreshAvds(context); context.UI.HideInvalidSdkPathBanner = false; const std::string &p = context.Host.Sdk.SdkPath; - strncpy(sdkPathBuffer, p.c_str(), sizeof(sdkPathBuffer) - 1); - sdkPathBuffer[sizeof(sdkPathBuffer) - 1] = '\0'; + strncpy(sdkPathBuffer, p.c_str(), bufferSize - 1); + sdkPathBuffer[bufferSize - 1] = '\0'; PersistAppSettings(context); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Forget the saved override and detect the SDK from ANDROID_HOME / default paths."); } + } + } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - { - constexpr float closeW = 120.0f; - ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - closeW + ImGui::GetCursorPosX()); - if (PrimaryButton("Close", true, ImVec2(closeW, 0))) { - context.UI.ShowPreferences = false; - ImGui::CloseCurrentPopup(); + void BuildPreferencesWindow(Context &context) { + if (context.UI.ShowPreferences && !ImGui::IsPopupOpen("Preferences###CoreDeckPrefs")) { + ImGui::OpenPopup("Preferences###CoreDeckPrefs"); + } + + const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(760, 480), ImGuiCond_Appearing); + + static char sdkPathBuffer[2048]; + static auto activeSection = PrefsSection::General; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + if (ImGui::BeginPopupModal("Preferences###CoreDeckPrefs", &context.UI.ShowPreferences, WindowNoResizeFlags)) { + ImGui::PopStyleVar(); + + if (ImGui::IsWindowAppearing()) { + const std::string &p = context.Host.Sdk.SdkPath; + strncpy(sdkPathBuffer, p.c_str(), sizeof(sdkPathBuffer) - 1); + sdkPathBuffer[sizeof(sdkPathBuffer) - 1] = '\0'; + } + + constexpr float sidebarW = 200.0f; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, HexColor(Colors::Surface0)); + ImGui::BeginChild("##PrefsSidebar", ImVec2(sidebarW, 0), false); + ImGui::PopStyleColor(); + ImGui::Dummy(ImVec2(0, 12)); + ImGui::SetWindowFontScale(1.4f); + const char *brand = "CoreDeck"; + const float brandW = ImGui::CalcTextSize(brand).x; + ImGui::SetCursorPosX((sidebarW - brandW) * 0.5f); + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextPrimary)); + ImGui::TextUnformatted(brand); + ImGui::PopStyleColor(); + ImGui::SetWindowFontScale(1.0f); + const char *version = "v" COREDECK_VERSION; + const float versionW = ImGui::CalcTextSize(version).x; + ImGui::SetCursorPosX((sidebarW - versionW) * 0.5f); + ImGui::PushStyleColor(ImGuiCol_Text, HexColor(Colors::TextMuted)); + ImGui::TextUnformatted(version); + ImGui::PopStyleColor(); + + ImGui::Dummy(ImVec2(0, 12)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 2)); + for (const auto &item: SidebarItems) { + if (SidebarRow(item, item.Section == activeSection)) { + activeSection = item.Section; } } + ImGui::PopStyleVar(); + ImGui::EndChild(); + + // Vertical divider + const ImVec2 popupPos = ImGui::GetWindowPos(); + const ImVec2 popupSize = ImGui::GetWindowSize(); + const float dividerX = popupPos.x + sidebarW; + ImGui::GetWindowDrawList()->AddLine( + ImVec2(dividerX, popupPos.y), + ImVec2(dividerX, popupPos.y + popupSize.y), + ImGui::GetColorU32(HexColor(Colors::BorderSubtle)), + 1.0f + ); + + ImGui::SameLine(0, 0); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 12.0f)); + ImGui::BeginChild("##PrefsContent", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::PopStyleVar(); + switch (activeSection) { + case PrefsSection::General: + DrawGeneralSection(context); + break; + case PrefsSection::AndroidSdk: + DrawAndroidSdkSection(context, sdkPathBuffer, sizeof(sdkPathBuffer)); + break; + } + ImGui::EndChild(); ImGui::EndPopup(); + } else { + ImGui::PopStyleVar(); } } -} +} \ No newline at end of file diff --git a/src/gui/windows/skin.cpp b/src/gui/windows/skin.cpp new file mode 100644 index 0000000..f06c769 --- /dev/null +++ b/src/gui/windows/skin.cpp @@ -0,0 +1,152 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#include +#include + +#include "imgui.h" + +#include "skin.h" +#include "../theme.h" +#include "../widgets.h" +#include "../../core/utilities.h" + +namespace CoreDeck { + static const char *SourceColor(const SkinSource &source) { + switch (source) { + case SkinSource::Sdk: + return Colors::AccentPhone; + case SkinSource::SystemImage: + return Colors::Positive; + case SkinSource::Platform: + return Colors::TextSubtle; + } + return Colors::TextSubtle; + } + + std::string SkinPreviewLabel(const Context &context) { + const auto &work = context.AvdCreationWork; + if (work.SelectedSkin <= 0 || work.Skins.empty()) { + return "No skin (plain emulator window)"; + } + const int idx = std::clamp(work.SelectedSkin - 1, 0, static_cast(work.Skins.size()) - 1); + const auto &s = work.Skins[idx]; + return StrConcat(s.DisplayName, " - ", SkinSourceLabel(s.Source)); + } + + static bool MatchesSkinFilter(const Skin &skin, const char *filter) { + if (!filter || filter[0] == '\0') return true; + return ContainsIgnoreCase(StrConcat(skin.Name, " ", skin.DisplayName), filter); + } + + void BuildSkinWindow(Context &context) { + if (!context.UI.ShowSkinDialog) return; + + constexpr auto title = "Choose Skin###SkinDialog"; + if (!ImGui::IsPopupOpen(title)) { + ImGui::OpenPopup(title); + } + + const ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(720, 500), ImGuiCond_Appearing); + + if (ImGui::BeginPopupModal(title, &context.UI.ShowSkinDialog, WindowAutoResizeFlags)) { + auto &work = context.AvdCreationWork; + const int totalRows = static_cast(work.Skins.size()) + 1; // +1 for "No skin" + work.PendingSelectedSkin = std::clamp(work.PendingSelectedSkin, 0, totalRows - 1); + + ImGui::SetNextItemWidth(-1.0f); + const std::string searchHint = IconWithLabel(Icons::Search, "Search skins..."); + ImGui::InputTextWithHint( + "##SkinSearch", + searchHint.c_str(), + work.SkinSearchFilter, + sizeof(work.SkinSearchFilter) + ); + + ImGui::Spacing(); + ImGui::Text("Skins"); + ImGui::Spacing(); + + { + PickerTableStyle ts; + ImGui::BeginChild("##SkinTableFrame", ImVec2(-1.0f, 320.0f), true, ImGuiWindowFlags_NoScrollbar); + if (ImGui::BeginTable("##SkinTable", 2, PickerTableFlags, ImVec2(-1.0f, -1.0f))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(" Name", ImGuiTableColumnFlags_WidthStretch, 2.8f); + ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthFixed, 140.0f); + ImGui::TableHeadersRow(); + + int visibleCount = 0; + + const bool noSkinSelected = work.PendingSelectedSkin == 0; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + const std::string noSkinLabel = StrConcat(" ", Icons::Gear, " No skin (plain emulator window)##SkinNone"); + if (ImGui::Selectable(noSkinLabel.c_str(), noSkinSelected, ImGuiSelectableFlags_SpanAllColumns)) { + work.PendingSelectedSkin = 0; + } + if (noSkinSelected) ImGui::SetItemDefaultFocus(); + ImGui::TableNextColumn(); + ImGui::TextDisabled("—"); + visibleCount++; + + for (int i = 0; i < static_cast(work.Skins.size()); i++) { + const auto &skin = work.Skins[i]; + if (!MatchesSkinFilter(skin, work.SkinSearchFilter)) continue; + + const int rowIndex = i + 1; + const bool isSelected = work.PendingSelectedSkin == rowIndex; + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + const std::string rowLabel = StrConcat(" ", Icons::Mobile, " ", skin.DisplayName, "##Skin", std::to_string(i)); + if (ImGui::Selectable(rowLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) { + work.PendingSelectedSkin = rowIndex; + } + if (isSelected) ImGui::SetItemDefaultFocus(); + + ImGui::TableNextColumn(); + ImGui::TextColored(HexColor(SourceColor(skin.Source)), "%s", SkinSourceLabel(skin.Source)); + visibleCount++; + } + + if (visibleCount == 0) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextDisabled("No skins match the search."); + } + + ImGui::EndTable(); + } + ImGui::EndChild(); + } + + if (work.Skins.empty()) { + ImGui::Spacing(); + ImGui::TextWrapped("No skins were found in your SDK. Skins typically ship with system images and the SDK skins folder."); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float halfWidth = (ImGui::GetContentRegionAvail().x - spacing) * 0.5f; + + if (PrimaryButton("Use Selected Skin", true, ImVec2(halfWidth, 0))) { + work.SelectedSkin = work.PendingSelectedSkin; + work.SkinAutoFilled = false; + context.UI.ShowSkinDialog = false; + } + ImGui::SameLine(); + if (PrimaryButton("Cancel", true, ImVec2(halfWidth, 0))) { + context.UI.ShowSkinDialog = false; + } + + ImGui::EndPopup(); + } + } +} \ No newline at end of file diff --git a/src/gui/windows/skin.h b/src/gui/windows/skin.h new file mode 100644 index 0000000..aea017f --- /dev/null +++ b/src/gui/windows/skin.h @@ -0,0 +1,18 @@ +// +// Created by AbdulMuaz Aqeel on 06/05/2026. +// + +#ifndef COREDECK_GUI_SKIN_H +#define COREDECK_GUI_SKIN_H + +#include + +#include "../context.h" + +namespace CoreDeck { + std::string SkinPreviewLabel(const Context &context); + + void BuildSkinWindow(Context &context); +} + +#endif // COREDECK_GUI_SKIN_H \ No newline at end of file diff --git a/src/gui/windows/storage.cpp b/src/gui/windows/storage.cpp index f1d31f1..e45c48e 100644 --- a/src/gui/windows/storage.cpp +++ b/src/gui/windows/storage.cpp @@ -50,8 +50,8 @@ namespace CoreDeck { static void DrawStorageSummaryCard(const char *title, const std::string &value, const char *accentColor, const float width) { StyleColor sc; StyleVar sv; - sc.push(ImGuiCol_ChildBg, HexColor("#141417")); - sc.push(ImGuiCol_Border, HexColor("#2E2E33")); + sc.push(ImGuiCol_ChildBg, HexColor(Colors::Surface1)); + sc.push(ImGuiCol_Border, HexColor(Colors::Surface4)); sv.push(ImGuiStyleVar_ChildRounding, 8.0f); sv.push(ImGuiStyleVar_ChildBorderSize, 1.0f); sv.push(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 12.0f)); @@ -71,14 +71,14 @@ namespace CoreDeck { const ImVec2 end(pos.x + width, pos.y + height); auto *drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(pos, end, ImGui::ColorConvertFloat4ToU32(HexColor("#1A1A1C")), 999.0f); + drawList->AddRectFilled(pos, end, ImGui::ColorConvertFloat4ToU32(HexColor(Colors::Surface2)), 999.0f); if (total > 0) { const float avdWidth = width * (static_cast(avdSize) / static_cast(total)); if (avdSize > 0) { - drawList->AddRectFilled(pos, ImVec2(pos.x + avdWidth, end.y), ImGui::ColorConvertFloat4ToU32(HexColor("#4D9AFF")), 999.0f); + drawList->AddRectFilled(pos, ImVec2(pos.x + avdWidth, end.y), ImGui::ColorConvertFloat4ToU32(HexColor(Colors::AccentInfo)), 999.0f); } if (systemImageSize > 0) { - drawList->AddRectFilled(ImVec2(pos.x + avdWidth, pos.y), end, ImGui::ColorConvertFloat4ToU32(HexColor("#33CC47")), 999.0f); + drawList->AddRectFilled(ImVec2(pos.x + avdWidth, pos.y), end, ImGui::ColorConvertFloat4ToU32(HexColor(Colors::Positive)), 999.0f); } } @@ -119,21 +119,21 @@ namespace CoreDeck { const float spacing = ImGui::GetStyle().ItemSpacing.x; const float cardWidth = (ImGui::GetContentRegionAvail().x - spacing * 2.0f) / 3.0f; - DrawStorageSummaryCard("Total Storage", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(grandTotal), "#F2F2F2", cardWidth); + DrawStorageSummaryCard("Total Storage", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(grandTotal), Colors::TextPrimary, cardWidth); ImGui::SameLine(); - DrawStorageSummaryCard("AVDs", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(TotalAvdSize), "#4D9AFF", cardWidth); + DrawStorageSummaryCard("AVDs", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(TotalAvdSize), Colors::AccentInfo, cardWidth); ImGui::SameLine(); - DrawStorageSummaryCard("System Images", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(SystemImagesSize), "#33CC47", cardWidth); + DrawStorageSummaryCard("System Images", isLoading && !disk.Ready ? "Calculating..." : FormatFileSize(SystemImagesSize), Colors::Positive, cardWidth); ImGui::Spacing(); ImGui::TextDisabled("Breakdown"); DrawStorageBreakdownBar(TotalAvdSize, SystemImagesSize); ImGui::Spacing(); - ImGui::TextColored(HexColor("#4D9AFF"), "AVDs"); + ImGui::TextColored(HexColor(Colors::AccentInfo), "AVDs"); ImGui::SameLine(); ImGui::TextDisabled("%s", FormatFileSize(TotalAvdSize).c_str()); ImGui::SameLine(); - ImGui::TextColored(HexColor("#33CC47"), "System Images"); + ImGui::TextColored(HexColor(Colors::Positive), "System Images"); ImGui::SameLine(); ImGui::TextDisabled("%s", FormatFileSize(SystemImagesSize).c_str()); diff --git a/src/gui/windows/update.cpp b/src/gui/windows/update.cpp index 073ed22..37c992b 100644 --- a/src/gui/windows/update.cpp +++ b/src/gui/windows/update.cpp @@ -28,7 +28,7 @@ namespace CoreDeck { ImGui::Spacing(); ImGui::Text("Current: "); ImGui::SameLine(0, 0.0f); - ImGui::TextColored(HexColor("#33CC47"), "v%s", COREDECK_VERSION); + ImGui::TextColored(HexColor(Colors::Positive), "v%s", COREDECK_VERSION); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -61,10 +61,10 @@ namespace CoreDeck { ImGui::TextUnformatted("Latest: "); ImGui::SameLine(0, 0.0f); - ImGui::TextColored(HexColor("#33CC47"), "%s", context.Updates.LatestVersion.c_str()); + ImGui::TextColored(HexColor(Colors::Positive), "%s", context.Updates.LatestVersion.c_str()); ImGui::TextUnformatted("Current: "); ImGui::SameLine(0, 0.0f); - ImGui::TextColored(HexColor("#FF9F40"), "v%s", COREDECK_VERSION); + ImGui::TextColored(HexColor(Colors::Warning), "v%s", COREDECK_VERSION); ImGui::Spacing(); ImGui::TextWrapped( "You will be taken to the CoreDeck website, where you can download and install the latest version." diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4eba9e4..f9bf713 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,8 @@ add_executable(coredeck_tests test_utilities.cpp test_paths.cpp test_log_buffer.cpp + test_log_filter.cpp + test_skin.cpp test_version_check.cpp ) diff --git a/tests/test_log_filter.cpp b/tests/test_log_filter.cpp new file mode 100644 index 0000000..82aa401 --- /dev/null +++ b/tests/test_log_filter.cpp @@ -0,0 +1,92 @@ +#include + +#include "core/log_filter.h" + +using namespace CoreDeck; + +namespace { + std::vector Sample() { + return { + "INFO | start emulator", + "WARNING | hvf is not enabled", + "ERROR | mprotect failed: Permission denied", + "INFO | retrying...", + }; + } + + std::string Slice(const std::string &joined, const LogMatch &m) { + return joined.substr(m.StartOffset, m.EndOffset - m.StartOffset); + } +} + +TEST_CASE("FilterLog with empty query returns joined text and no matches", "[log_filter][substring]") { + const auto result = FilterLog(Sample(), {}); + REQUIRE(result.Matches.empty()); + REQUIRE(result.RegexValid); + REQUIRE(result.Joined.find("hvf is not enabled") != std::string::npos); + REQUIRE(result.Joined.back() == '\n'); +} + +TEST_CASE("FilterLog substring match is case-insensitive by default", "[log_filter][substring]") { + const auto result = FilterLog(Sample(), {.Query = "WARNING"}); + REQUIRE(result.Matches.size() == 1); + REQUIRE(Slice(result.Joined, result.Matches[0]) == "WARNING"); + + const auto lowered = FilterLog(Sample(), {.Query = "warning"}); + REQUIRE(lowered.Matches.size() == 1); + REQUIRE(Slice(lowered.Joined, lowered.Matches[0]) == "WARNING"); +} + +TEST_CASE("FilterLog substring match honors case-sensitive flag", "[log_filter][substring]") { + LogFilterOptions opts; + opts.Query = "warning"; + opts.CaseSensitive = true; + const auto result = FilterLog(Sample(), opts); + REQUIRE(result.Matches.empty()); +} + +TEST_CASE("FilterLog finds multiple substring occurrences across lines", "[log_filter][substring]") { + const auto result = FilterLog(Sample(), {.Query = "INFO"}); + REQUIRE(result.Matches.size() == 2); + for (const auto &m: result.Matches) { + REQUIRE(Slice(result.Joined, m) == "INFO"); + } + REQUIRE(result.Matches[0].StartOffset < result.Matches[1].StartOffset); +} + +TEST_CASE("FilterLog regex compiles and matches", "[log_filter][regex]") { + LogFilterOptions opts; + opts.Query = "(WARNING|ERROR)"; + opts.UseRegex = true; + const auto result = FilterLog(Sample(), opts); + REQUIRE(result.RegexValid); + REQUIRE(result.Matches.size() == 2); + REQUIRE(Slice(result.Joined, result.Matches[0]) == "WARNING"); + REQUIRE(Slice(result.Joined, result.Matches[1]) == "ERROR"); +} + +TEST_CASE("FilterLog reports invalid regex without crashing", "[log_filter][regex]") { + LogFilterOptions opts; + opts.Query = "(unclosed"; + opts.UseRegex = true; + const auto [Joined, Matches, RegexValid, RegexError] = FilterLog(Sample(), opts); + REQUIRE_FALSE(RegexValid); + REQUIRE_FALSE(RegexError.empty()); + REQUIRE(Matches.empty()); + REQUIRE(Joined.find("hvf is not enabled") != std::string::npos); +} + +TEST_CASE("FilterLog regex zero-width matches are skipped", "[log_filter][regex]") { + LogFilterOptions opts; + opts.Query = "^"; + opts.UseRegex = true; + const auto result = FilterLog({"a", "b"}, opts); + REQUIRE(result.RegexValid); + REQUIRE(result.Matches.empty()); +} + +TEST_CASE("FilterLog handles empty input", "[log_filter][edge]") { + const auto result = FilterLog({}, {.Query = "anything"}); + REQUIRE(result.Joined.empty()); + REQUIRE(result.Matches.empty()); +} \ No newline at end of file diff --git a/tests/test_skin.cpp b/tests/test_skin.cpp new file mode 100644 index 0000000..8977a68 --- /dev/null +++ b/tests/test_skin.cpp @@ -0,0 +1,107 @@ +#include + +#include +#include +#include + +#include "core/skin.h" + +namespace fs = std::filesystem; +using namespace CoreDeck; + +namespace { + fs::path UniqueTempDir(const std::string &prefix) { + std::random_device rd; + const fs::path base = fs::temp_directory_path() / (prefix + "_" + std::to_string(rd())); + fs::create_directories(base); + return base; + } + + void MakeSkin(const fs::path &dir) { + fs::create_directories(dir); + std::ofstream(dir / "layout") << "parts {}\n"; + } +} + +TEST_CASE("ListSkins discovers skins from $SDK/skins", "[skin][list]") { + const fs::path sdk = UniqueTempDir("coredeck_skin_sdk"); + MakeSkin(sdk / "skins" / "pixel_7"); + MakeSkin(sdk / "skins" / "WearOSRound"); + + SdkInfo info; + info.SdkPath = sdk.string(); + + const auto skins = ListSkins(info); + REQUIRE(skins.size() == 2); + + bool sawPixel = false, sawWear = false; + for (const auto &s: skins) { + if (s.Name == "pixel_7") { + sawPixel = true; + REQUIRE(s.Source == SkinSource::Sdk); + } + if (s.Name == "WearOSRound") sawWear = true; + } + REQUIRE(sawPixel); + REQUIRE(sawWear); + + fs::remove_all(sdk); +} + +TEST_CASE("ListSkins prefers SDK-level skin over system-image-bundled duplicate", "[skin][dedup]") { + const fs::path sdk = UniqueTempDir("coredeck_skin_dedup"); + MakeSkin(sdk / "skins" / "pixel_7"); + MakeSkin(sdk / "system-images" / "android-34" / "google_apis" / "arm64-v8a" / "skins" / "pixel_7"); + + SdkInfo info; + info.SdkPath = sdk.string(); + + const auto skins = ListSkins(info); + REQUIRE(skins.size() == 1); + REQUIRE(skins[0].Name == "pixel_7"); + REQUIRE(skins[0].Source == SkinSource::Sdk); + + fs::remove_all(sdk); +} + +TEST_CASE("ListSkins ignores directories without a layout file", "[skin][list]") { + const fs::path sdk = UniqueTempDir("coredeck_skin_invalid"); + fs::create_directories(sdk / "skins" / "not_a_skin"); + + SdkInfo info; + info.SdkPath = sdk.string(); + + const auto skins = ListSkins(info); + REQUIRE(skins.empty()); + + fs::remove_all(sdk); +} + +TEST_CASE("FindSkinForDevice returns exact-name match when present", "[skin][match]") { + std::vector skins = { + {"pixel_6", "Pixel 6", "/p6", SkinSource::Sdk}, + {"pixel_7", "Pixel 7", "/p7", SkinSource::Sdk}, + }; + + const auto match = FindSkinForDevice(skins, "pixel_7"); + REQUIRE(match.has_value()); + REQUIRE(match->Name == "pixel_7"); +} + +TEST_CASE("FindSkinForDevice returns nullopt for unknown device", "[skin][match]") { + const std::vector skins = { + {"pixel_7", "Pixel 7", "/p7", SkinSource::Sdk}, + }; + + const auto match = FindSkinForDevice(skins, "totally_unknown_device_xyz"); + REQUIRE_FALSE(match.has_value()); +} + +TEST_CASE("FindSkinForDevice handles empty inputs", "[skin][match]") { + REQUIRE_FALSE(FindSkinForDevice({}, "pixel_7").has_value()); + + const std::vector skins = { + {"pixel_7", "Pixel 7", "/p7", SkinSource::Sdk} + }; + REQUIRE_FALSE(FindSkinForDevice(skins, "").has_value()); +} \ No newline at end of file From 0e05034f192a051b712b2e57ef5a9e18a05f1265 Mon Sep 17 00:00:00 2001 From: devmuaz Date: Thu, 7 May 2026 15:41:08 +0300 Subject: [PATCH 2/2] Add .clang-tidy file --- .clang-tidy | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..446d55d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,96 @@ +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-narrowing-conversions, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-owning-memory, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + performance-*, + readability-*, + -readability-identifier-length, + -readability-magic-numbers, + -readability-named-parameter, + -readability-function-cognitive-complexity, + misc-*, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + -misc-include-cleaner, + portability-* + +WarningsAsErrors: '' +HeaderFilterRegex: '^src/.*\.(h|hpp)$' +FormatStyle: file + +CheckOptions: + - key: readability-identifier-naming.NamespaceCase + value: CamelCase + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.UnionCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.MethodCase + value: CamelCase + - key: readability-identifier-naming.MemberCase + value: CamelCase + - key: readability-identifier-naming.PublicMemberCase + value: CamelCase + - key: readability-identifier-naming.ProtectedMemberCase + value: CamelCase + - key: readability-identifier-naming.PrivateMemberCase + value: CamelCase + - key: readability-identifier-naming.ParameterCase + value: camelBack + - key: readability-identifier-naming.LocalVariableCase + value: camelBack + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-identifier-naming.StaticVariableCase + value: camelBack + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.ConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.ClassConstantCase + value: CamelCase + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + - key: readability-identifier-naming.TemplateParameterCase + value: CamelCase + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: true + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: true + - key: modernize-use-default-member-init.UseAssignment + value: true + - key: modernize-use-override.IgnoreDestructors + value: true + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: true + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: true \ No newline at end of file