Load and render proper icons

Signed-off-by: Slendi <slendi@socopon.com>
This commit is contained in:
2025-10-15 03:33:35 +03:00
parent aaf5dbb3b7
commit 81584c643e
6 changed files with 368 additions and 18 deletions

View File

@@ -52,6 +52,14 @@ FetchContent_Declare(
) )
FetchContent_MakeAvailable(mINI) 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) find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
@@ -176,6 +184,7 @@ target_link_libraries(waylight PRIVATE
raylib raylib
msdfgen::msdfgen-core msdfgen::msdfgen-core
msdfgen::msdfgen-ext msdfgen::msdfgen-ext
lunasvg::lunasvg
m m
dl dl

View File

@@ -194,6 +194,7 @@ auto App::run() -> void
SetWindowSize(m_win_w, m_win_h); SetWindowSize(m_win_w, m_win_h);
while (m_running) { while (m_running) {
pump_events(); pump_events();
m_ir.color(m_accent_color);
tick(); tick();
m_kbd.typing.clear(); m_kbd.typing.clear();
m_kbd.clear_transients(); m_kbd.clear_transients();

View File

@@ -22,6 +22,7 @@ extern "C" {
#include <wayland-egl.h> #include <wayland-egl.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include "IconRegistry.hpp"
#include "ImGui.hpp" #include "ImGui.hpp"
#include "TextRenderer.hpp" #include "TextRenderer.hpp"
#include "Theme.hpp" #include "Theme.hpp"
@@ -217,6 +218,7 @@ private:
enum_array<Theme, ColorScheme> m_themes { make_default_themes() }; enum_array<Theme, ColorScheme> m_themes { make_default_themes() };
Theme m_active_theme { Theme::Light }; Theme m_active_theme { Theme::Light };
IconRegistry m_ir;
int m_win_w { 800 }; int m_win_w { 800 };
int m_win_h { 600 }; int m_win_h { 600 };

View File

@@ -1,12 +1,205 @@
#include "IconRegistry.hpp" #include "IconRegistry.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <filesystem> #include <filesystem>
#include <print>
#include <ranges> #include <ranges>
#include <stdexcept> #include <stdexcept>
#include <lunasvg.h>
#include <mini/ini.h> #include <mini/ini.h>
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<std::string> 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<int> 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<unsigned char> 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) IconTheme::IconTheme(std::filesystem::path const &themes_directory_path)
{ {
for (auto const &dir : for (auto const &dir :
@@ -15,15 +208,23 @@ IconTheme::IconTheme(std::filesystem::path const &themes_directory_path)
continue; continue;
auto const index_path = dir.path() / "index.theme"; 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; continue;
m_names.push_back(dir.path().filename().string());
mINI::INIFile ini_file(index_path); mINI::INIFile ini_file(index_path);
mINI::INIStructure ini; mINI::INIStructure ini;
ini_file.read(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"] }; 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( auto const dir_entry_str { std::string(
dir_entry.begin(), dir_entry.end()) }; dir_entry.begin(), dir_entry.end()) };
auto const path { std::filesystem::path(dir_entry_str) }; auto const path { std::filesystem::path(dir_entry_str) };
@@ -44,34 +245,56 @@ IconTheme::IconTheme(std::filesystem::path const &themes_directory_path)
continue; continue;
} }
auto const &context_raw { ini[dir_entry_str]["context"] }; auto const &context_raw { ini[dir_entry_str]["Context"] };
DirectoryEntry::Context context; DirectoryEntry::Context context;
if (context_raw == "Actions") { if (context_raw == "Actions") {
context = DirectoryEntry::Context::Actions; context = DirectoryEntry::Context::Actions;
} else if (type_raw == "Devices") { } else if (context_raw == "Devices") {
context = DirectoryEntry::Context::Devices; context = DirectoryEntry::Context::Devices;
} else if (type_raw == "FileSystems") { } else if (context_raw == "FileSystems") {
context = DirectoryEntry::Context::FileSystems; context = DirectoryEntry::Context::FileSystems;
} else if (type_raw == "MomeTypes") { } else if (context_raw == "MimeTypes") {
context = DirectoryEntry::Context::MimeTypes; context = DirectoryEntry::Context::MimeTypes;
} else if (context_raw == "Places") {
context = DirectoryEntry::Context::Places;
} else { } else {
continue; continue;
} }
int size { std::atoi(ini[dir_entry_str]["Size"].c_str()) }; 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({ m_directories.push_back({
.path = path, .path = path_actual,
.size = size, .size = size,
.type = type, .type = type,
.context = context, .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() IconRegistry::IconRegistry()
{ {
m_preferred_theme = get_current_icon_theme();
std::vector<std::filesystem::path> theme_directory_paths; std::vector<std::filesystem::path> theme_directory_paths;
{ {
@@ -85,19 +308,29 @@ IconRegistry::IconRegistry()
{ {
auto const *env { getenv("XDG_DATA_DIRS") }; auto const *env { getenv("XDG_DATA_DIRS") };
if (env && *env) { if (env && *env) {
theme_directory_paths.push_back( std::ranges::copy(std::string_view(env) | std::views::split(':')
std::filesystem::path(env) / "icons"); | 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" }; std::filesystem::path const paths[] {
if (std::filesystem::exists(path)) "/usr/share/pixmaps",
theme_directory_paths.push_back(path); "/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) for (auto &&path : theme_directory_paths
| std::views::filter([](std::filesystem::path &path) { | std::views::filter([](std::filesystem::path const &path) {
return std::filesystem::is_directory(path); return std::filesystem::is_directory(path);
})) { })) {
try { try {
@@ -109,4 +342,71 @@ IconRegistry::IconRegistry()
if (m_themes.empty()) { if (m_themes.empty()) {
throw std::runtime_error("Could not find any icon themes."); 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<int> optimal_size, bool symbolic, std::optional<Color> 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<int> 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));
} }

View File

@@ -1,20 +1,45 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <optional>
#include <span> #include <span>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
struct Icon { }; #include <raylib.h>
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 { struct IconTheme {
IconTheme(std::filesystem::path const &themes_directory_path); IconTheme(std::filesystem::path const &themes_directory_path);
~IconTheme() = default; ~IconTheme() = default;
auto inherits() const -> std::span<std::string const> constexpr auto inherits() const -> std::span<std::string const>
{ {
return std::span { m_inherits }; return std::span { m_inherits };
} }
auto lookup(std::string_view const name,
std::optional<int> optimal_size = std::nullopt) const -> Icon const;
auto names() const -> std::vector<std::string> const & { return m_names; }
private: private:
struct DirectoryEntry { struct DirectoryEntry {
@@ -29,6 +54,7 @@ private:
Devices, Devices,
FileSystems, FileSystems,
MimeTypes, MimeTypes,
Places,
}; };
std::filesystem::path path; std::filesystem::path path;
@@ -37,7 +63,7 @@ private:
Context context; Context context;
}; };
std::unordered_map<std::string, Icon> m_cached_icons; std::vector<std::string> m_names;
std::vector<std::string> m_inherits; std::vector<std::string> m_inherits;
std::vector<DirectoryEntry> m_directories; std::vector<DirectoryEntry> m_directories;
}; };
@@ -46,6 +72,18 @@ struct IconRegistry {
IconRegistry(); IconRegistry();
~IconRegistry() = default; ~IconRegistry() = default;
auto lookup(std::string_view const name,
std::optional<int> optimal_size = std::nullopt, bool symbolic = false,
std::optional<Color> color = std::nullopt) -> Icon const &;
auto color(std::optional<Color> const &color) { m_color = color; }
private: private:
std::optional<Color> m_color { std::nullopt };
auto lookup_cached(std::string_view const name,
std::optional<int> optimal_size) -> Icon const &;
std::vector<IconTheme> m_themes; std::vector<IconTheme> m_themes;
std::unordered_map<std::string, Icon> m_cached_icons;
std::optional<std::string> m_preferred_theme;
}; };

View File

@@ -65,7 +65,7 @@ auto App::tick() -> void
update_text_input_state(text_input_data, 1, input_rect); 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(); EndDrawing();