From 81584c643e46d33f18c257db34f6744aeaa17af4 Mon Sep 17 00:00:00 2001 From: Slendi Date: Wed, 15 Oct 2025 03:33:35 +0300 Subject: [PATCH] Load and render proper icons Signed-off-by: Slendi --- CMakeLists.txt | 9 ++ src/App.cpp | 1 + src/App.hpp | 2 + src/IconRegistry.cpp | 328 +++++++++++++++++++++++++++++++++++++++++-- src/IconRegistry.hpp | 44 +++++- src/Tick.cpp | 2 +- 6 files changed, 368 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c85e1ae..ef49572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,14 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(mINI) +FetchContent_Declare( + lunasvg + GIT_REPOSITORY https://github.com/sammycage/lunasvg.git + GIT_TAG "v3.5.0" + GIT_SHALLOW 1 +) +FetchContent_MakeAvailable(lunasvg) + find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) @@ -176,6 +184,7 @@ target_link_libraries(waylight PRIVATE raylib msdfgen::msdfgen-core msdfgen::msdfgen-ext + lunasvg::lunasvg m dl diff --git a/src/App.cpp b/src/App.cpp index 6d5e33f..7f954f6 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -194,6 +194,7 @@ auto App::run() -> void SetWindowSize(m_win_w, m_win_h); while (m_running) { pump_events(); + m_ir.color(m_accent_color); tick(); m_kbd.typing.clear(); m_kbd.clear_transients(); diff --git a/src/App.hpp b/src/App.hpp index b08d3ee..64817d7 100644 --- a/src/App.hpp +++ b/src/App.hpp @@ -22,6 +22,7 @@ extern "C" { #include #include +#include "IconRegistry.hpp" #include "ImGui.hpp" #include "TextRenderer.hpp" #include "Theme.hpp" @@ -217,6 +218,7 @@ private: enum_array m_themes { make_default_themes() }; Theme m_active_theme { Theme::Light }; + IconRegistry m_ir; int m_win_w { 800 }; int m_win_h { 600 }; diff --git a/src/IconRegistry.cpp b/src/IconRegistry.cpp index 55dda53..eb657e0 100644 --- a/src/IconRegistry.cpp +++ b/src/IconRegistry.cpp @@ -1,12 +1,205 @@ #include "IconRegistry.hpp" +#include +#include +#include #include +#include #include +#include #include #include +#include #include +static inline auto color_to_string(Color const &c) -> std::string +{ + auto const r { c.r / 255.0 }, g { c.g / 255.0 }, b { c.b / 255.0 }; + + auto const maxv { std::fmax(r, std::fmax(g, b)) }; + auto const minv { std::fmin(r, std::fmin(g, b)) }; + auto const d { maxv - minv }; + + double h = 0.0; + if (d > 1e-6) { + if (maxv == r) + h = 60.0 * std::fmod(((g - b) / d), 6.0); + else if (maxv == g) + h = 60.0 * (((b - r) / d) + 2.0); + else + h = 60.0 * (((r - g) / d) + 4.0); + } + if (h < 0.0) + h += 360.0; + + if (h >= 345 || h < 15) + return "red"; + if (h < 45) + return "orange"; + if (h < 70) + return "yellow"; + if (h < 170) + return "green"; + if (h < 200) + return "teal"; + if (h < 250) + return "cyan"; + if (h < 290) + return "blue"; + if (h < 330) + return "purple"; + return "pink"; +} + +static auto detect_desktop_environment() -> std::string const +{ + if (auto const de { getenv("XDG_CURRENT_DESKTOP") }) + return de; + if (auto const sess { getenv("DESKTOP_SESSION") }) + return sess; + return "unknown"; +} + +static auto kde_get_theme() -> std::string const +{ + std::string home { getenv("HOME") ? getenv("HOME") : "" }; + + std::string const paths[] { + home + "/.config/kdeglobals", + home + "/.config/kdedefaults/kdeglobals", + }; + + for (auto p : paths) { + std::ifstream f(p); + if (!f) + continue; + std::string line; + auto in_icons { false }; + while (std::getline(f, line)) { + if (line == "[Icons]") { + in_icons = true; + continue; + } + if (line.starts_with("[")) + in_icons = false; + if (in_icons && line.starts_with("Theme=")) + return line.substr(strlen("Theme=")); + } + } + return {}; +} + +static auto get_current_icon_theme() -> std::optional const +{ + auto de { detect_desktop_environment() }; + std::transform(de.begin(), de.end(), de.begin(), ::tolower); + + if (de.find("kde") != std::string::npos + || de.find("plasma") != std::string::npos) { + if (auto const t = kde_get_theme(); !t.empty()) { + return t; + } + } + + return std::nullopt; +} + +auto IconTheme::lookup(std::string_view const name, + std::optional optimal_size) const -> Icon const +{ + for (auto const &dir : m_directories) { + if (optimal_size && *optimal_size < dir.size) + continue; + + for (auto const &dir_entry : + std::filesystem::recursive_directory_iterator(dir.path)) { + if (!dir_entry.is_regular_file()) + continue; + + if (dir_entry.path().stem() != name) + continue; + + // This can be derived from the image filename. + // But we probably won't need it either way... + if (dir_entry.path().extension() == ".icon") + continue; + + if (dir_entry.path().extension() == ".svg") { + auto const document { lunasvg::Document::loadFromFile( + dir_entry.path()) }; + if (!document) { + throw std::runtime_error("Failed to load SVG file"); + } + + auto const bitmap { document->renderToBitmap() }; + if (bitmap.width() == 0 || bitmap.height() == 0) + continue; + + std::vector rgba( + bitmap.width() * bitmap.height() * 4); + auto *src = bitmap.data(); + for (size_t i = 0, px = bitmap.width() * bitmap.height(); + i < px; ++i) { + uint8_t b = src[i * 4 + 0]; + uint8_t g = src[i * 4 + 1]; + uint8_t r = src[i * 4 + 2]; + uint8_t a = src[i * 4 + 3]; + + if (a != 0) { + r = (uint8_t)std::min( + 255, (int)((r * 255 + a / 2) / a)); + g = (uint8_t)std::min( + 255, (int)((g * 255 + a / 2) / a)); + b = (uint8_t)std::min( + 255, (int)((b * 255 + a / 2) / a)); + } + + rgba[i * 4 + 0] = r; + rgba[i * 4 + 1] = g; + rgba[i * 4 + 2] = b; + rgba[i * 4 + 3] = a; + } + + Image const img { + .data = rgba.data(), + .width = bitmap.width(), + .height = bitmap.height(), + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + }; + + auto const tex { LoadTextureFromImage(img) }; + if (!IsTextureValid(tex)) { + throw std::runtime_error( + "Failed to load texture from image"); + } + + Icon const icon(dir_entry.path(), tex, dir.size); + return icon; + } else { + auto const tex { LoadTexture(dir_entry.path().c_str()) }; + if (!IsTextureValid(tex)) { + throw std::runtime_error( + "Failed to load texture from file"); + } + + Icon const icon(dir_entry.path(), tex, dir.size); + return icon; + } + } + } + + if (optimal_size) { + // We failed to find a icon big enough, try again with smaller sizes + // than our optimal. + return lookup(name, std::nullopt); + } + + throw std::runtime_error( + std::format("Failed to find icon `{}` in theme!", name)); +} + IconTheme::IconTheme(std::filesystem::path const &themes_directory_path) { for (auto const &dir : @@ -15,15 +208,23 @@ IconTheme::IconTheme(std::filesystem::path const &themes_directory_path) continue; auto const index_path = dir.path() / "index.theme"; - if (std::filesystem::is_regular_file(index_path)) + if (!std::filesystem::is_regular_file(index_path)) continue; + m_names.push_back(dir.path().filename().string()); + mINI::INIFile ini_file(index_path); mINI::INIStructure ini; ini_file.read(ini); + auto const &inherits { ini["Icon Theme"]["Inherits"] }; + std::ranges::copy(std::string_view(inherits) | std::views::split(',') + | std::views::transform( + [](auto &&s) { return std::string(s.begin(), s.end()); }), + std::back_inserter(m_inherits)); + auto const &directories { ini["Icon Theme"]["Directories"] }; - for (auto const &&dir_entry : directories | std::views::split(':')) { + for (auto const &&dir_entry : directories | std::views::split(',')) { auto const dir_entry_str { std::string( dir_entry.begin(), dir_entry.end()) }; auto const path { std::filesystem::path(dir_entry_str) }; @@ -44,34 +245,56 @@ IconTheme::IconTheme(std::filesystem::path const &themes_directory_path) continue; } - auto const &context_raw { ini[dir_entry_str]["context"] }; + auto const &context_raw { ini[dir_entry_str]["Context"] }; DirectoryEntry::Context context; if (context_raw == "Actions") { context = DirectoryEntry::Context::Actions; - } else if (type_raw == "Devices") { + } else if (context_raw == "Devices") { context = DirectoryEntry::Context::Devices; - } else if (type_raw == "FileSystems") { + } else if (context_raw == "FileSystems") { context = DirectoryEntry::Context::FileSystems; - } else if (type_raw == "MomeTypes") { + } else if (context_raw == "MimeTypes") { context = DirectoryEntry::Context::MimeTypes; + } else if (context_raw == "Places") { + context = DirectoryEntry::Context::Places; } else { continue; } int size { std::atoi(ini[dir_entry_str]["Size"].c_str()) }; + if (size == 0) { + if (type == DirectoryEntry::Type::Scalable) { + int minSize + = std::atoi(ini[dir_entry_str]["MinSize"].c_str()); + int maxSize + = std::atoi(ini[dir_entry_str]["MaxSize"].c_str()); + size = std::max(minSize, maxSize); + } + if (size == 0) + continue; + } m_directories.push_back({ - .path = path, + .path = path_actual, .size = size, .type = type, .context = context, }); } + + // Sort by biggest sizes first. This is important for the lookup + // algorithm. Mess with this, change that. + std::sort(m_directories.begin(), m_directories.end(), + [](DirectoryEntry const &a, DirectoryEntry const &b) { + return a.size > b.size; + }); } } IconRegistry::IconRegistry() { + m_preferred_theme = get_current_icon_theme(); + std::vector theme_directory_paths; { @@ -85,19 +308,29 @@ IconRegistry::IconRegistry() { auto const *env { getenv("XDG_DATA_DIRS") }; if (env && *env) { - theme_directory_paths.push_back( - std::filesystem::path(env) / "icons"); + std::ranges::copy(std::string_view(env) | std::views::split(':') + | std::views::transform([](auto &&s) { + return std::filesystem::path(s.begin(), s.end()) + / "icons"; + }), + std::back_inserter(theme_directory_paths)); } } { - std::filesystem::path const path { "/usr/share/pixmaps" }; - if (std::filesystem::exists(path)) - theme_directory_paths.push_back(path); + std::filesystem::path const paths[] { + "/usr/share/pixmaps", + "/usr/local/share/icons", + "/usr/share/icons", + }; + for (auto const &path : paths) { + if (std::filesystem::exists(path)) + theme_directory_paths.push_back(path); + } } - for (auto &&path : std::move(theme_directory_paths) - | std::views::filter([](std::filesystem::path &path) { + for (auto &&path : theme_directory_paths + | std::views::filter([](std::filesystem::path const &path) { return std::filesystem::is_directory(path); })) { try { @@ -109,4 +342,71 @@ IconRegistry::IconRegistry() if (m_themes.empty()) { throw std::runtime_error("Could not find any icon themes."); } + + if (m_preferred_theme) { + TraceLog(LOG_INFO, + std::format("Preferred theme: {}", *m_preferred_theme).c_str()); + + std::stable_partition( + m_themes.begin(), m_themes.end(), [&](auto const &t) { + bool found { false }; + for (auto const &e : t.names()) { + if (e == *m_preferred_theme) { + found = true; + break; + } + } + return found; + }); + } +} + +auto IconRegistry::lookup(std::string_view const name, + std::optional optimal_size, bool symbolic, std::optional color) + -> Icon const & +{ + if (!color && m_color) + color = m_color; + + std::string color_name {}; + if (color) { + auto const col { color_to_string(*color) }; + if (!col.empty()) { + color_name = "-" + col; + } + } + if (symbolic) { + try { + auto const n { std::format("{}{}-symbolic", color_name, name) }; + return lookup_cached(n, optimal_size); + } catch (...) { + return lookup(name, optimal_size, false, color); + } + } else { + return lookup_cached( + std::string_view(std::format("{}{}", name, color_name)), + optimal_size); + } +} + +auto IconRegistry::lookup_cached(std::string_view const name, + std::optional optimal_size) -> Icon const & +{ + std::string name_s(name); + if (m_cached_icons.contains(name_s)) { + auto const &icon = m_cached_icons.at(name_s); + if (optimal_size && icon.size() >= *optimal_size) + return icon; + } + + for (auto const &theme : m_themes) { + try { + auto const icon = theme.lookup(name, optimal_size); + m_cached_icons.insert_or_assign(name_s, icon); + return m_cached_icons.at(name_s); + } catch (...) { + } + } + + throw std::runtime_error(std::format("Failed to find icon `{}`!", name)); } diff --git a/src/IconRegistry.hpp b/src/IconRegistry.hpp index 796d9b6..a4aba02 100644 --- a/src/IconRegistry.hpp +++ b/src/IconRegistry.hpp @@ -1,20 +1,45 @@ #pragma once #include +#include #include #include #include -struct Icon { }; +#include + +struct Icon { + Icon(std::filesystem::path path, Texture2D texture, int size) + : m_path(path) + , m_texture(texture) + , m_size(size) + { + } + + constexpr auto path() const -> std::filesystem::path const & + { + return m_path; + } + constexpr auto texture() const -> Texture2D const & { return m_texture; } + constexpr auto size() const -> int const & { return m_size; } + +private: + std::filesystem::path m_path; + Texture2D m_texture; + int m_size { 0 }; +}; struct IconTheme { IconTheme(std::filesystem::path const &themes_directory_path); ~IconTheme() = default; - auto inherits() const -> std::span + constexpr auto inherits() const -> std::span { return std::span { m_inherits }; } + auto lookup(std::string_view const name, + std::optional optimal_size = std::nullopt) const -> Icon const; + auto names() const -> std::vector const & { return m_names; } private: struct DirectoryEntry { @@ -29,6 +54,7 @@ private: Devices, FileSystems, MimeTypes, + Places, }; std::filesystem::path path; @@ -37,7 +63,7 @@ private: Context context; }; - std::unordered_map m_cached_icons; + std::vector m_names; std::vector m_inherits; std::vector m_directories; }; @@ -46,6 +72,18 @@ struct IconRegistry { IconRegistry(); ~IconRegistry() = default; + auto lookup(std::string_view const name, + std::optional optimal_size = std::nullopt, bool symbolic = false, + std::optional color = std::nullopt) -> Icon const &; + auto color(std::optional const &color) { m_color = color; } + private: + std::optional m_color { std::nullopt }; + + auto lookup_cached(std::string_view const name, + std::optional optimal_size) -> Icon const &; + std::vector m_themes; + std::unordered_map m_cached_icons; + std::optional m_preferred_theme; }; diff --git a/src/Tick.cpp b/src/Tick.cpp index 7576249..32f5495 100644 --- a/src/Tick.cpp +++ b/src/Tick.cpp @@ -65,7 +65,7 @@ auto App::tick() -> void update_text_input_state(text_input_data, 1, input_rect); } - // DrawTexture(get_texture(*icon_lookup("folder", 32)), 50, 50, WHITE); + DrawTexture(m_ir.lookup("folder", 48).texture(), 48, 48, WHITE); EndDrawing();