Load and render proper icons
Signed-off-by: Slendi <slendi@socopon.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -22,6 +22,7 @@ extern "C" {
|
||||
#include <wayland-egl.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
#include "IconRegistry.hpp"
|
||||
#include "ImGui.hpp"
|
||||
#include "TextRenderer.hpp"
|
||||
#include "Theme.hpp"
|
||||
@@ -217,6 +218,7 @@ private:
|
||||
|
||||
enum_array<Theme, ColorScheme> m_themes { make_default_themes() };
|
||||
Theme m_active_theme { Theme::Light };
|
||||
IconRegistry m_ir;
|
||||
|
||||
int m_win_w { 800 };
|
||||
int m_win_h { 600 };
|
||||
|
||||
@@ -1,12 +1,205 @@
|
||||
#include "IconRegistry.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <print>
|
||||
#include <ranges>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <lunasvg.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)
|
||||
{
|
||||
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<std::filesystem::path> 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<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));
|
||||
}
|
||||
|
||||
@@ -1,20 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#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 {
|
||||
IconTheme(std::filesystem::path const &themes_directory_path);
|
||||
~IconTheme() = default;
|
||||
|
||||
auto inherits() const -> std::span<std::string const>
|
||||
constexpr auto inherits() const -> std::span<std::string const>
|
||||
{
|
||||
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:
|
||||
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<std::string, Icon> m_cached_icons;
|
||||
std::vector<std::string> m_names;
|
||||
std::vector<std::string> m_inherits;
|
||||
std::vector<DirectoryEntry> m_directories;
|
||||
};
|
||||
@@ -46,6 +72,18 @@ struct IconRegistry {
|
||||
IconRegistry();
|
||||
~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:
|
||||
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::unordered_map<std::string, Icon> m_cached_icons;
|
||||
std::optional<std::string> m_preferred_theme;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user