hyprcursor/libhyprcursor/hyprcursor.cpp

784 lines
27 KiB
C++
Raw Normal View History

2024-03-07 17:36:26 +01:00
#include "hyprcursor/hyprcursor.hpp"
2024-03-07 04:19:38 +01:00
#include "internalSharedTypes.hpp"
#include "internalDefines.hpp"
#include <array>
#include <filesystem>
#include <hyprlang.hpp>
#include <zip.h>
#include <cstring>
#include <algorithm>
2024-03-07 21:46:36 +01:00
#include <librsvg/rsvg.h>
2024-03-07 04:19:38 +01:00
#include "Log.hpp"
using namespace Hyprcursor;
// directories for lookup
constexpr const std::array<const char*, 1> systemThemeDirs = {"/usr/share/icons"};
constexpr const std::array<const char*, 2> userThemeDirs = {"/.local/share/icons", "/.icons"};
//
2024-03-24 21:37:31 +01:00
static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) {
2024-03-07 04:19:38 +01:00
const auto ENV = getenv("HYPRCURSOR_THEME");
2024-03-24 21:37:31 +01:00
if (!ENV) {
Debug::log(HC_LOG_INFO, logfn, "themeNameFromEnv: env unset");
2024-03-07 04:19:38 +01:00
return "";
2024-03-24 21:37:31 +01:00
}
2024-03-07 04:19:38 +01:00
return std::string{ENV};
}
static bool themeAccessible(const std::string& path) {
try {
if (!std::filesystem::exists(path + "/manifest.hl"))
return false;
} catch (std::exception& e) { return false; }
return true;
}
2024-03-24 21:37:31 +01:00
static std::string getFirstTheme(PHYPRCURSORLOGFUNC logfn) {
2024-03-07 04:19:38 +01:00
// try user directories first
const auto HOMEENV = getenv("HOME");
if (!HOMEENV)
return "";
const std::string HOME{HOMEENV};
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(FULLPATH) || !std::filesystem::exists(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
2024-03-07 04:19:38 +01:00
continue;
2024-03-26 16:26:26 +01:00
}
2024-03-07 04:19:38 +01:00
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
2024-03-07 04:19:38 +01:00
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
2024-03-24 21:37:31 +01:00
if (std::filesystem::exists(MANIFESTPATH)) {
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
2024-03-07 04:19:38 +01:00
return themeDir.path().stem().string();
2024-03-24 21:37:31 +01:00
}
2024-03-07 04:19:38 +01:00
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(FULLPATH) || !std::filesystem::exists(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
2024-03-07 04:19:38 +01:00
continue;
2024-03-26 16:26:26 +01:00
}
2024-03-07 04:19:38 +01:00
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
2024-03-07 04:19:38 +01:00
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
2024-03-26 16:26:26 +01:00
if (!std::filesystem::exists(MANIFESTPATH)) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logfn, "getFirstTheme: found {}", themeDir.path().string());
2024-03-07 04:19:38 +01:00
return themeDir.path().stem().string();
2024-03-24 21:37:31 +01:00
}
2024-03-07 04:19:38 +01:00
}
}
return "";
}
2024-03-24 21:37:31 +01:00
static std::string getFullPathForThemeName(const std::string& name, PHYPRCURSORLOGFUNC logfn) {
2024-03-07 04:19:38 +01:00
const auto HOMEENV = getenv("HOME");
if (!HOMEENV)
return "";
const std::string HOME{HOMEENV};
for (auto& dir : userThemeDirs) {
const auto FULLPATH = HOME + dir;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(FULLPATH) || !std::filesystem::exists(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
2024-03-07 04:19:38 +01:00
continue;
2024-03-26 16:26:26 +01:00
}
2024-03-07 04:19:38 +01:00
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
}
2024-03-21 16:42:18 +01:00
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
if (name.empty()) {
2024-03-24 21:37:31 +01:00
if (std::filesystem::exists(MANIFESTPATH)) {
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
2024-03-21 16:42:18 +01:00
return std::filesystem::canonical(themeDir.path()).string();
2024-03-24 21:37:31 +01:00
}
2024-03-09 18:45:39 +01:00
continue;
2024-03-21 16:42:18 +01:00
}
2024-03-09 18:45:39 +01:00
2024-03-21 16:42:18 +01:00
if (!std::filesystem::exists(MANIFESTPATH))
continue;
2024-03-07 04:19:38 +01:00
2024-03-21 16:42:18 +01:00
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* e) { continue; }
const std::string NAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
2024-03-25 12:54:29 +01:00
if (NAME != name && name != themeDir.path().stem().string())
2024-03-21 16:42:18 +01:00
continue;
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
2024-03-21 16:42:18 +01:00
return std::filesystem::canonical(themeDir.path()).string();
2024-03-07 04:19:38 +01:00
}
}
for (auto& dir : systemThemeDirs) {
const auto FULLPATH = dir;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(FULLPATH) || !std::filesystem::exists(FULLPATH)) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping path {} because it's inaccessible.", FULLPATH);
2024-03-07 04:19:38 +01:00
continue;
2024-03-26 16:26:26 +01:00
}
2024-03-07 04:19:38 +01:00
// loop over dirs and see if any has a manifest.hl
for (auto& themeDir : std::filesystem::directory_iterator(FULLPATH)) {
if (!themeDir.is_directory())
continue;
2024-03-26 16:26:26 +01:00
if (!themeAccessible(themeDir.path().string())) {
Debug::log(HC_LOG_TRACE, logfn, "Skipping theme {} because it's inaccessible.", themeDir.path().string());
continue;
2024-03-26 16:26:26 +01:00
}
2024-03-07 04:19:38 +01:00
const auto MANIFESTPATH = themeDir.path().string() + "/manifest.hl";
2024-03-25 12:54:29 +01:00
if (!std::filesystem::exists(MANIFESTPATH))
continue;
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
manifest = std::make_unique<Hyprlang::CConfig>(MANIFESTPATH.c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("name", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* e) { continue; }
const std::string NAME = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("name"));
if (NAME != name && name != themeDir.path().stem().string())
continue;
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: found {}", themeDir.path().string());
return std::filesystem::canonical(themeDir.path()).string();
2024-03-07 04:19:38 +01:00
}
}
2024-03-24 21:37:31 +01:00
if (!name.empty()) { // try without name
Debug::log(HC_LOG_INFO, logfn, "getFullPathForThemeName: failed, trying without name of {}", name);
return getFullPathForThemeName("", logfn);
}
2024-03-09 18:45:39 +01:00
2024-03-07 04:19:38 +01:00
return "";
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_) {
2024-03-24 21:37:31 +01:00
init(themeName_);
}
CHyprcursorManager::CHyprcursorManager(const char* themeName_, PHYPRCURSORLOGFUNC fn) {
logFn = fn;
init(themeName_);
}
void CHyprcursorManager::init(const char* themeName_) {
2024-03-07 04:19:38 +01:00
std::string themeName = themeName_ ? themeName_ : "";
if (themeName.empty()) {
// try reading from env
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find theme from env");
themeName = themeNameFromEnv(logFn);
2024-03-07 04:19:38 +01:00
}
if (themeName.empty()) {
// try finding first, in the hierarchy
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: attempting to find any theme");
themeName = getFirstTheme(logFn);
2024-03-07 04:19:38 +01:00
}
if (themeName.empty()) {
// holy shit we're done
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "CHyprcursorManager: no themes matched");
2024-03-07 04:19:38 +01:00
return;
}
// initialize theme
2024-03-24 21:37:31 +01:00
impl = new CHyprcursorImplementation(this, logFn);
2024-03-07 04:19:38 +01:00
impl->themeName = themeName;
2024-03-24 21:37:31 +01:00
impl->themeFullDir = getFullPathForThemeName(themeName, logFn);
2024-03-07 04:19:38 +01:00
if (impl->themeFullDir.empty())
return;
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "Found theme {} at {}\n", impl->themeName, impl->themeFullDir);
2024-03-07 04:19:38 +01:00
const auto LOADSTATUS = impl->loadTheme();
if (LOADSTATUS.has_value()) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Theme failed to load with {}\n", LOADSTATUS.value());
2024-03-07 04:19:38 +01:00
return;
}
finalizedAndValid = true;
}
CHyprcursorManager::~CHyprcursorManager() {
if (impl)
delete impl;
}
bool CHyprcursorManager::valid() {
return finalizedAndValid;
}
2024-03-07 17:01:45 +01:00
SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info) {
2024-03-24 21:37:31 +01:00
if (!shape_) {
Debug::log(HC_LOG_ERR, logFn, "getShapesC: shape of nullptr is invalid");
return nullptr;
}
2024-03-07 17:01:45 +01:00
std::string REQUESTEDSHAPE = shape_;
std::vector<SLoadedCursorImage*> resultingImages;
2024-03-07 18:51:14 +01:00
float hotX = 0, hotY = 0;
2024-03-07 04:19:38 +01:00
for (auto& shape : impl->theme.shapes) {
2024-03-07 17:21:04 +01:00
if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end())
2024-03-07 04:19:38 +01:00
continue;
2024-03-07 18:51:14 +01:00
hotX = shape->hotspotX;
hotY = shape->hotspotY;
2024-03-07 04:19:38 +01:00
// matched :)
2024-03-07 17:01:45 +01:00
bool foundAny = false;
2024-03-07 17:21:04 +01:00
for (auto& image : impl->loadedShapes[shape.get()].images) {
2024-03-11 22:01:14 +01:00
if (image->side != info.size)
2024-03-07 04:19:38 +01:00
continue;
2024-03-07 15:50:48 +01:00
// found size
2024-03-07 17:01:45 +01:00
resultingImages.push_back(image.get());
foundAny = true;
2024-03-07 04:19:38 +01:00
}
2024-03-07 21:46:36 +01:00
if (foundAny || shape->shapeType == SHAPE_SVG /* something broke, this shouldn't happen with svg */)
2024-03-07 17:01:45 +01:00
break;
2024-03-07 15:50:48 +01:00
// if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error.
2024-03-07 17:21:04 +01:00
if (shape->resizeAlgo != RESIZE_NONE) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match a size?");
2024-03-07 15:50:48 +01:00
return nullptr;
}
// find nearest
2024-03-07 17:01:45 +01:00
int leader = 13371337;
2024-03-07 17:21:04 +01:00
for (auto& image : impl->loadedShapes[shape.get()].images) {
2024-03-07 15:50:48 +01:00
if (std::abs((int)(image->side - info.size)) > leader)
continue;
2024-03-07 17:01:45 +01:00
leader = image->side;
2024-03-07 15:50:48 +01:00
}
2024-03-07 17:01:45 +01:00
if (leader == 13371337) { // ???
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size?");
2024-03-07 15:50:48 +01:00
return nullptr;
}
2024-03-07 17:01:45 +01:00
// we found nearest size
2024-03-07 17:21:04 +01:00
for (auto& image : impl->loadedShapes[shape.get()].images) {
2024-03-07 17:01:45 +01:00
if (image->side != leader)
continue;
// found size
resultingImages.push_back(image.get());
foundAny = true;
}
if (foundAny)
break;
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match any nearest size (2)?");
2024-03-07 17:01:45 +01:00
return nullptr;
2024-03-07 04:19:38 +01:00
}
2024-03-07 15:50:48 +01:00
2024-03-07 17:01:45 +01:00
// alloc and return what we need
SCursorImageData** data = (SCursorImageData**)malloc(sizeof(SCursorImageData*) * resultingImages.size());
for (size_t i = 0; i < resultingImages.size(); ++i) {
2024-03-07 18:51:14 +01:00
data[i] = (SCursorImageData*)malloc(sizeof(SCursorImageData));
data[i]->delay = resultingImages[i]->delay;
data[i]->size = resultingImages[i]->side;
data[i]->surface = resultingImages[i]->cairoSurface;
data[i]->hotspotX = hotX * data[i]->size;
data[i]->hotspotY = hotY * data[i]->size;
2024-03-07 17:01:45 +01:00
}
outSize = resultingImages.size();
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "getShapesC: found {} images for {}", outSize, shape_);
2024-03-07 17:01:45 +01:00
return data;
2024-03-07 04:19:38 +01:00
}
2024-03-07 15:50:48 +01:00
bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_INFO, logFn, "loadThemeStyle: loading for size {}", info.size);
2024-03-07 15:50:48 +01:00
for (auto& shape : impl->theme.shapes) {
2024-03-24 21:37:31 +01:00
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG) {
// don't resample NONE style cursors
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: ignoring {}", shape->directory);
continue;
}
2024-03-07 15:50:48 +01:00
bool sizeFound = false;
2024-03-07 21:46:36 +01:00
if (shape->shapeType == SHAPE_PNG) {
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != info.size)
continue;
2024-03-07 15:50:48 +01:00
2024-03-07 21:46:36 +01:00
sizeFound = true;
break;
}
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
// size wasn't found, let's resample.
2024-03-07 21:46:36 +01:00
if (sizeFound)
2024-03-07 15:50:48 +01:00
continue;
2024-03-07 21:46:36 +01:00
SLoadedCursorImage* leader = nullptr;
int leaderVal = 1000000;
2024-03-07 17:21:04 +01:00
for (auto& image : impl->loadedShapes[shape.get()].images) {
2024-03-07 21:46:36 +01:00
if (image->side < info.size)
continue;
if (image->side > leaderVal)
2024-03-07 15:50:48 +01:00
continue;
leaderVal = image->side;
leader = image.get();
}
2024-03-07 21:46:36 +01:00
if (!leader) {
for (auto& image : impl->loadedShapes[shape.get()].images) {
if (std::abs((int)(image->side - info.size)) > leaderVal)
continue;
leaderVal = image->side;
leader = image.get();
}
}
if (!leader) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Resampling failed to find a candidate???");
2024-03-07 21:46:36 +01:00
return false;
}
2024-03-11 22:01:14 +01:00
const auto FRAMES = impl->getFramesFor(shape.get(), leader->side);
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape {} has {} frames", shape->directory, FRAMES.size());
2024-03-11 22:01:14 +01:00
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->delay = f->delay;
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
const auto PCAIRO = cairo_create(newImage->cairoSurface);
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
cairo_save(PCAIRO);
cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR);
cairo_paint(PCAIRO);
cairo_restore(PCAIRO);
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
const auto PTN = cairo_pattern_create_for_surface(f->cairoSurface);
cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE);
const float scale = info.size / (float)f->side;
cairo_scale(PCAIRO, scale, scale);
cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_set_source(PCAIRO, PTN);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
cairo_rectangle(PCAIRO, 0, 0, info.size, info.size);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
cairo_fill(PCAIRO);
cairo_surface_flush(newImage->cairoSurface);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
cairo_pattern_destroy(PTN);
cairo_destroy(PCAIRO);
}
2024-03-07 21:46:36 +01:00
} else if (shape->shapeType == SHAPE_SVG) {
2024-03-11 22:01:14 +01:00
const auto FRAMES = impl->getFramesFor(shape.get(), 0);
2024-03-07 15:50:48 +01:00
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape {} has {} frames", shape->directory, FRAMES.size());
2024-03-11 22:01:14 +01:00
for (auto& f : FRAMES) {
auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true;
newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4];
newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4);
newImage->delay = f->delay;
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
const auto PCAIRO = cairo_create(newImage->cairoSurface);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
cairo_save(PCAIRO);
cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR);
cairo_paint(PCAIRO);
cairo_restore(PCAIRO);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
GError* error = nullptr;
RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)f->data, f->dataLen, &error);
2024-03-07 15:50:48 +01:00
2024-03-11 22:01:14 +01:00
if (!handle) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Failed reading svg: {}", error->message);
2024-03-11 22:01:14 +01:00
return false;
}
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size};
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message);
2024-03-11 22:01:14 +01:00
return false;
}
2024-03-07 21:46:36 +01:00
2024-03-11 22:01:14 +01:00
// done
cairo_surface_flush(newImage->cairoSurface);
cairo_destroy(PCAIRO);
}
2024-03-07 21:46:36 +01:00
} else {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Invalid shapetype in loadThemeStyle");
2024-03-07 21:46:36 +01:00
return false;
}
2024-03-07 15:50:48 +01:00
}
return true;
}
void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
for (auto& shape : impl->theme.shapes) {
2024-03-11 22:04:17 +01:00
if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG)
2024-03-07 15:50:48 +01:00
continue;
2024-03-11 23:01:40 +01:00
std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) {
2024-03-11 22:04:17 +01:00
const bool isSVG = shape->shapeType == SHAPE_SVG;
const bool isArtificial = e->artificial;
// clean artificial rasters made for this
if (isArtificial && e->side == info.size)
return true;
// clean invalid non-svg rasters
if (!isSVG && e->side == 0)
return true;
return false;
});
2024-03-07 15:50:48 +01:00
}
2024-03-07 04:19:38 +01:00
}
2024-03-24 21:37:31 +01:00
void CHyprcursorManager::registerLoggingFunction(PHYPRCURSORLOGFUNC fn) {
logFn = fn;
}
2024-03-07 04:19:38 +01:00
/*
Implementation
*/
static std::string removeBeginEndSpacesTabs(std::string str) {
if (str.empty())
return str;
int countBefore = 0;
while (str[countBefore] == ' ' || str[countBefore] == '\t') {
countBefore++;
}
int countAfter = 0;
while ((int)str.length() - countAfter - 1 >= 0 && (str[str.length() - countAfter - 1] == ' ' || str[str.length() - 1 - countAfter] == '\t')) {
countAfter++;
}
str = str.substr(countBefore, str.length() - countBefore - countAfter);
return str;
}
SCursorTheme* currentTheme;
static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
if (!VALUE.contains(",")) {
result.setError("Invalid define_size");
return result;
}
2024-03-07 17:21:04 +01:00
auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
2024-03-07 04:19:38 +01:00
SCursorImage image;
2024-03-07 17:21:04 +01:00
if (RHS.contains(",")) {
const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(",")));
const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1));
try {
image.delay = std::stoull(RR);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
RHS = LL;
}
2024-03-07 04:19:38 +01:00
image.filename = RHS;
if (!image.filename.ends_with(".svg")) {
try {
image.size = std::stoull(LHS);
} catch (std::exception& e) {
result.setError(e.what());
return result;
}
} else
image.size = 0;
2024-03-07 04:19:38 +01:00
2024-03-07 17:21:04 +01:00
currentTheme->shapes.back()->images.push_back(image);
2024-03-07 04:19:38 +01:00
return result;
}
static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result;
const std::string VALUE = V;
2024-03-07 17:21:04 +01:00
currentTheme->shapes.back()->overrides.push_back(V);
2024-03-07 04:19:38 +01:00
return result;
}
/*
PNG reading
*/
static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int len) {
const auto DATA = (SLoadedCursorImage*)data;
if (!DATA->data)
return CAIRO_STATUS_READ_ERROR;
size_t toRead = len > DATA->dataLen - DATA->readNeedle ? DATA->dataLen - DATA->readNeedle : len;
std::memcpy(output, (uint8_t*)DATA->data + DATA->readNeedle, toRead);
2024-03-07 04:19:38 +01:00
DATA->readNeedle += toRead;
if (DATA->readNeedle >= DATA->dataLen) {
2024-03-26 16:26:26 +01:00
delete[](char*) DATA->data;
2024-03-07 04:19:38 +01:00
DATA->data = nullptr;
}
return CAIRO_STATUS_SUCCESS;
}
/*
General
*/
std::optional<std::string> CHyprcursorImplementation::loadTheme() {
if (!themeAccessible(themeFullDir))
return "Theme inaccessible";
2024-03-07 04:19:38 +01:00
currentTheme = &theme;
// load manifest
std::unique_ptr<Hyprlang::CConfig> manifest;
try {
// TODO: unify this between util and lib
manifest = std::make_unique<Hyprlang::CConfig>((themeFullDir + "/manifest.hl").c_str(), Hyprlang::SConfigOptions{});
manifest->addConfigValue("cursors_directory", Hyprlang::STRING{""});
manifest->commence();
manifest->parse();
} catch (const char* err) {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_ERR, logFn, "Failed parsing manifest due to {}", err);
2024-03-07 04:19:38 +01:00
return std::string{"failed: "} + err;
}
const std::string CURSORSSUBDIR = std::any_cast<Hyprlang::STRING>(manifest->getConfigValue("cursors_directory"));
const std::string CURSORDIR = themeFullDir + "/" + CURSORSSUBDIR;
if (CURSORSSUBDIR.empty() || !std::filesystem::exists(CURSORDIR))
return "loadTheme: cursors_directory missing or empty";
for (auto& cursor : std::filesystem::directory_iterator(CURSORDIR)) {
2024-03-24 21:37:31 +01:00
if (!cursor.is_regular_file()) {
Debug::log(HC_LOG_TRACE, logFn, "loadTheme: skipping {}", cursor.path().string());
2024-03-07 04:19:38 +01:00
continue;
2024-03-24 21:37:31 +01:00
}
2024-03-07 04:19:38 +01:00
2024-03-07 17:21:04 +01:00
auto& SHAPE = theme.shapes.emplace_back(std::make_unique<SCursorShape>());
auto& LOADEDSHAPE = loadedShapes[SHAPE.get()];
2024-03-07 04:19:38 +01:00
// extract zip to raw data.
int errp = 0;
zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp);
zip_file_t* meta_file = zip_fopen(zip, "meta.hl", ZIP_FL_UNCHANGED);
if (!meta_file)
return "cursor" + cursor.path().string() + "failed to load meta";
char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */
int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1);
zip_fclose(meta_file);
if (readBytes < 0) {
delete[] buffer;
return "cursor" + cursor.path().string() + "failed to read meta";
}
buffer[readBytes] = '\0';
std::unique_ptr<Hyprlang::CConfig> meta;
try {
meta = std::make_unique<Hyprlang::CConfig>(buffer, Hyprlang::SConfigOptions{.pathIsStream = true});
meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F});
meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F});
meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"});
meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false});
meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false});
meta->commence();
meta->parse();
2024-03-24 21:37:31 +01:00
} catch (const char* err) {
delete[] buffer;
return "failed parsing meta: " + std::string{err};
}
2024-03-07 04:19:38 +01:00
delete[] buffer;
2024-03-07 17:21:04 +01:00
for (auto& i : SHAPE->images) {
2024-03-07 21:46:36 +01:00
if (SHAPE->shapeType == SHAPE_INVALID) {
if (i.filename.ends_with(".svg"))
SHAPE->shapeType = SHAPE_SVG;
else if (i.filename.ends_with(".png"))
SHAPE->shapeType = SHAPE_PNG;
else {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_WARN, logFn, "WARNING: image {} has no known extension, assuming png.", i.filename);
2024-03-07 21:46:36 +01:00
SHAPE->shapeType = SHAPE_PNG;
}
} else {
if (SHAPE->shapeType == SHAPE_SVG && !i.filename.ends_with(".svg"))
return "meta invalid: cannot add .png files to an svg shape";
else if (SHAPE->shapeType == SHAPE_PNG && i.filename.ends_with(".svg"))
return "meta invalid: cannot add .svg files to a png shape";
}
2024-03-07 04:19:38 +01:00
// load image
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_TRACE, logFn, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
2024-03-11 22:01:14 +01:00
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
IMAGE->side = SHAPE->shapeType == SHAPE_SVG ? 0 : i.size;
2024-03-11 22:01:14 +01:00
IMAGE->delay = i.delay;
2024-03-12 01:25:08 +01:00
IMAGE->isSVG = SHAPE->shapeType == SHAPE_SVG;
2024-03-07 04:19:38 +01:00
// read from zip
zip_file_t* image_file = zip_fopen(zip, i.filename.c_str(), ZIP_FL_UNCHANGED);
if (!image_file)
return "cursor" + cursor.path().string() + "failed to load image_file";
IMAGE->data = new char[1024 * 1024]; /* 1MB should be more than enough, again. This probably should be in the spec. */
IMAGE->dataLen = zip_fread(image_file, IMAGE->data, 1024 * 1024 - 1);
zip_fclose(image_file);
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_TRACE, logFn, "Cairo: set up surface read");
2024-03-07 21:46:36 +01:00
if (SHAPE->shapeType == SHAPE_PNG) {
2024-03-07 04:19:38 +01:00
2024-03-07 21:46:36 +01:00
IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE);
2024-03-07 04:19:38 +01:00
2024-03-07 21:46:36 +01:00
if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) {
2024-03-26 16:26:26 +01:00
delete[](char*) IMAGE->data;
2024-03-07 21:46:36 +01:00
IMAGE->data = nullptr;
return "Failed reading cairoSurface, status " + std::to_string((int)STATUS);
}
} else {
2024-03-24 21:37:31 +01:00
Debug::log(HC_LOG_TRACE, logFn, "Skipping cairo load for a svg surface");
2024-03-07 04:19:38 +01:00
}
}
2024-03-07 17:21:04 +01:00
if (SHAPE->images.empty())
2024-03-07 04:19:38 +01:00
return "meta invalid: no images for shape " + cursor.path().stem().string();
2024-03-07 17:21:04 +01:00
SHAPE->directory = cursor.path().stem().string();
SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
2024-03-07 04:19:38 +01:00
zip_discard(zip);
}
return {};
2024-03-11 22:01:14 +01:00
}
std::vector<SLoadedCursorImage*> CHyprcursorImplementation::getFramesFor(SCursorShape* shape, int size) {
std::vector<SLoadedCursorImage*> frames;
for (auto& image : loadedShapes[shape].images) {
if (!image->isSVG && image->side != size)
continue;
if (image->artificial)
continue;
frames.push_back(image.get());
}
return frames;
2024-03-24 21:37:31 +01:00
}