all: add support for animated cursors

This commit is contained in:
Vaxry 2024-03-07 16:21:04 +00:00
parent 6760e68a1b
commit 3dc5ca4e11
7 changed files with 94 additions and 58 deletions

View File

@ -33,7 +33,7 @@ It provides C and C++ bindings.
## TODO ## TODO
Library: Library:
- [ ] Support animated cursors - [x] Support animated cursors
- [ ] Support SVG cursors - [ ] Support SVG cursors
Util: Util:

View File

@ -47,10 +47,26 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
return result; return result;
} }
const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image; SCursorImage image;
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;
}
image.filename = RHS; image.filename = RHS;
try { try {
@ -60,7 +76,7 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
return result; return result;
} }
currentTheme->shapes.back().images.push_back(image); currentTheme->shapes.back()->images.push_back(image);
return result; return result;
} }
@ -69,7 +85,7 @@ static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result; Hyprlang::CParseResult result;
const std::string VALUE = V; const std::string VALUE = V;
currentTheme->shapes.back().overrides.push_back(V); currentTheme->shapes.back()->overrides.push_back(V);
return result; return result;
} }
@ -107,7 +123,7 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) {
const auto METAPATH = dir.path().string() + "/meta.hl"; const auto METAPATH = dir.path().string() + "/meta.hl";
auto& SHAPE = currentTheme->shapes.emplace_back(); auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique<SCursorShape>());
// //
std::unique_ptr<Hyprlang::CConfig> meta; std::unique_ptr<Hyprlang::CConfig> meta;
@ -124,21 +140,21 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
} catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; } } catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; }
// check if we have at least one image. // check if we have at least one image.
for (auto& i : SHAPE.images) { for (auto& i : SHAPE->images) {
if (!std::filesystem::exists(dir.path().string() + "/" + i.filename)) if (!std::filesystem::exists(dir.path().string() + "/" + i.filename))
return "meta invalid: image " + i.filename + " does not exist"; return "meta invalid: image " + i.filename + " does not exist";
break; break;
} }
if (SHAPE.images.empty()) if (SHAPE->images.empty())
return "meta invalid: no images for shape " + dir.path().stem().string(); return "meta invalid: no images for shape " + dir.path().stem().string();
SHAPE.directory = dir.path().stem().string(); SHAPE->directory = dir.path().stem().string();
SHAPE.hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x")); SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE.hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y")); SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE.resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm"))); SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
std::cout << "Shape " << SHAPE.directory << ": \n\toverrides: " << SHAPE.overrides.size() << "\n\tsizes: " << SHAPE.images.size() << "\n"; std::cout << "Shape " << SHAPE->directory << ": \n\toverrides: " << SHAPE->overrides.size() << "\n\tsizes: " << SHAPE->images.size() << "\n";
} }
// create output fs structure // create output fs structure
@ -158,8 +174,8 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
// create zips (.hlc) for each // create zips (.hlc) for each
for (auto& shape : currentTheme->shapes) { for (auto& shape : currentTheme->shapes) {
const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape.directory; const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape->directory;
const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape.directory + ".hlc"; const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape->directory + ".hlc";
int errp = 0; int errp = 0;
zip_t* zip = zip_open(OUTPUTFILE.c_str(), ZIP_CREATE | ZIP_EXCL, &errp); zip_t* zip = zip_open(OUTPUTFILE.c_str(), ZIP_CREATE | ZIP_EXCL, &errp);
@ -179,14 +195,14 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
meta = nullptr; meta = nullptr;
// add each cursor png // add each cursor png
for (auto& i : shape.images) { for (auto& i : shape->images) {
zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, 0); zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, 0);
if (!image) if (!image)
return "(1) failed to add image " + (CURRENTCURSORSDIR + "/" + i.filename) + " to hlc"; return "(1) failed to add image " + (CURRENTCURSORSDIR + "/" + i.filename) + " to hlc";
if (zip_file_add(zip, (i.filename).c_str(), image, ZIP_FL_ENC_UTF_8) < 0) if (zip_file_add(zip, (i.filename).c_str(), image, ZIP_FL_ENC_UTF_8) < 0)
return "(2) failed to add image " + i.filename + " to hlc"; return "(2) failed to add image " + i.filename + " to hlc";
std::cout << "Added image " << i.filename << " to shape " << shape.directory << "\n"; std::cout << "Added image " << i.filename << " to shape " << shape->directory << "\n";
} }
// close zip and write // close zip and write
@ -326,7 +342,7 @@ static std::optional<std::string> extractXTheme(const std::string& xpath, const
for (auto& entry : entries) { for (auto& entry : entries) {
const auto ENTRYSTEM = entry.image.substr(entry.image.find_last_of('/') + 1); const auto ENTRYSTEM = entry.image.substr(entry.image.find_last_of('/') + 1);
metaString += std::format("define_size = {}, {}\n", entry.size, ENTRYSTEM); metaString += std::format("define_size = {}, {}, {}\n", entry.size, ENTRYSTEM, entry.delay);
} }
metaString += "\n"; metaString += "\n";

View File

@ -168,12 +168,12 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
std::vector<SLoadedCursorImage*> resultingImages; std::vector<SLoadedCursorImage*> resultingImages;
for (auto& shape : impl->theme.shapes) { for (auto& shape : impl->theme.shapes) {
if (REQUESTEDSHAPE != shape.directory && std::find(shape.overrides.begin(), shape.overrides.end(), REQUESTEDSHAPE) == shape.overrides.end()) if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end())
continue; continue;
// matched :) // matched :)
bool foundAny = false; bool foundAny = false;
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != info.size) if (image->side != info.size)
continue; continue;
@ -186,14 +186,14 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
break; break;
// if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error. // if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error.
if (shape.resizeAlgo != RESIZE_NONE) { if (shape->resizeAlgo != RESIZE_NONE) {
Debug::log(ERR, "getSurfaceFor didn't match a size?"); Debug::log(ERR, "getSurfaceFor didn't match a size?");
return nullptr; return nullptr;
} }
// find nearest // find nearest
int leader = 13371337; int leader = 13371337;
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (std::abs((int)(image->side - info.size)) > leader) if (std::abs((int)(image->side - info.size)) > leader)
continue; continue;
@ -206,7 +206,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
} }
// we found nearest size // we found nearest size
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != leader) if (image->side != leader)
continue; continue;
@ -238,12 +238,12 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
for (auto& shape : impl->theme.shapes) { for (auto& shape : impl->theme.shapes) {
if (shape.resizeAlgo == RESIZE_NONE) if (shape->resizeAlgo == RESIZE_NONE)
continue; // don't resample NONE style cursors continue; // don't resample NONE style cursors
bool sizeFound = false; bool sizeFound = false;
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side != info.size) if (image->side != info.size)
continue; continue;
@ -257,7 +257,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
// size wasn't found, let's resample. // size wasn't found, let's resample.
SLoadedCursorImage* leader = nullptr; SLoadedCursorImage* leader = nullptr;
int leaderVal = 1000000; int leaderVal = 1000000;
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side < info.size) if (image->side < info.size)
continue; continue;
@ -269,10 +269,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
} }
if (!leader) { if (!leader) {
for (auto& image : impl->loadedShapes[&shape].images) { for (auto& image : impl->loadedShapes[shape.get()].images) {
if (image->side < info.size)
continue;
if (std::abs((int)(image->side - info.size)) > leaderVal) if (std::abs((int)(image->side - info.size)) > leaderVal)
continue; continue;
@ -286,7 +283,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
return false; return false;
} }
auto& newImage = impl->loadedShapes[&shape].images.emplace_back(std::make_unique<SLoadedCursorImage>()); auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique<SLoadedCursorImage>());
newImage->artificial = true; newImage->artificial = true;
newImage->side = info.size; newImage->side = info.size;
newImage->artificialData = new char[info.size * info.size * 4]; newImage->artificialData = new char[info.size * info.size * 4];
@ -294,7 +291,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
const auto PCAIRO = cairo_create(newImage->cairoSurface); const auto PCAIRO = cairo_create(newImage->cairoSurface);
cairo_set_antialias(PCAIRO, shape.resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE);
cairo_save(PCAIRO); cairo_save(PCAIRO);
cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR);
@ -305,7 +302,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE);
const float scale = info.size / (float)leader->side; const float scale = info.size / (float)leader->side;
cairo_scale(PCAIRO, scale, scale); cairo_scale(PCAIRO, scale, scale);
cairo_pattern_set_filter(PTN, shape.resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST);
cairo_set_source(PCAIRO, PTN); cairo_set_source(PCAIRO, PTN);
cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); cairo_rectangle(PCAIRO, 0, 0, info.size, info.size);
@ -322,10 +319,10 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) {
void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) {
for (auto& shape : impl->theme.shapes) { for (auto& shape : impl->theme.shapes) {
if (shape.resizeAlgo == RESIZE_NONE) if (shape->resizeAlgo == RESIZE_NONE)
continue; continue;
std::erase_if(impl->loadedShapes[&shape].images, [info](const auto& e) { return !e->artificial && (info.size == 0 || e->side == info.size); }); std::erase_if(impl->loadedShapes[shape.get()].images, [info](const auto& e) { return !e->artificial && (info.size == 0 || e->side == info.size); });
} }
} }
@ -365,10 +362,26 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
return result; return result;
} }
const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(",")));
const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1));
auto DELAY = 0;
SCursorImage image; SCursorImage image;
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;
}
image.filename = RHS; image.filename = RHS;
try { try {
@ -378,7 +391,7 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) {
return result; return result;
} }
currentTheme->shapes.back().images.push_back(image); currentTheme->shapes.back()->images.push_back(image);
return result; return result;
} }
@ -387,7 +400,7 @@ static Hyprlang::CParseResult parseOverride(const char* C, const char* V) {
Hyprlang::CParseResult result; Hyprlang::CParseResult result;
const std::string VALUE = V; const std::string VALUE = V;
currentTheme->shapes.back().overrides.push_back(V); currentTheme->shapes.back()->overrides.push_back(V);
return result; return result;
} }
@ -451,8 +464,8 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
if (!cursor.is_regular_file()) if (!cursor.is_regular_file())
continue; continue;
auto& SHAPE = theme.shapes.emplace_back(); auto& SHAPE = theme.shapes.emplace_back(std::make_unique<SCursorShape>());
auto& LOADEDSHAPE = loadedShapes[&SHAPE]; auto& LOADEDSHAPE = loadedShapes[SHAPE.get()];
// extract zip to raw data. // extract zip to raw data.
int errp = 0; int errp = 0;
@ -490,7 +503,7 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
delete[] buffer; delete[] buffer;
for (auto& i : SHAPE.images) { for (auto& i : SHAPE->images) {
// load image // load image
Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string());
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get(); auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
@ -511,6 +524,8 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE);
IMAGE->delay = i.delay;
if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) {
delete[] (char*)IMAGE->data; delete[] (char*)IMAGE->data;
IMAGE->data = nullptr; IMAGE->data = nullptr;
@ -518,13 +533,13 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
} }
} }
if (SHAPE.images.empty()) if (SHAPE->images.empty())
return "meta invalid: no images for shape " + cursor.path().stem().string(); return "meta invalid: no images for shape " + cursor.path().stem().string();
SHAPE.directory = cursor.path().stem().string(); SHAPE->directory = cursor.path().stem().string();
SHAPE.hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x")); SHAPE->hotspotX = std::any_cast<float>(meta->getConfigValue("hotspot_x"));
SHAPE.hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y")); SHAPE->hotspotY = std::any_cast<float>(meta->getConfigValue("hotspot_y"));
SHAPE.resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm"))); SHAPE->resizeAlgo = stringToAlgo(std::any_cast<Hyprlang::STRING>(meta->getConfigValue("resize_algorithm")));
zip_discard(zip); zip_discard(zip);
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory>
enum eResizeAlgo { enum eResizeAlgo {
RESIZE_NONE = 0, RESIZE_NONE = 0,
@ -31,5 +32,5 @@ struct SCursorShape {
}; };
struct SCursorTheme { struct SCursorTheme {
std::vector<SCursorShape> shapes; std::vector<std::unique_ptr<SCursorShape>> shapes;
}; };

View File

@ -11,10 +11,14 @@ int main(int argc, char** argv) {
} }
// get cursor for left_ptr // get cursor for left_ptr
const auto SHAPEDATA = mgr.getShape("left_ptr", Hyprcursor::SCursorStyleInfo{.size = 48}); const auto SHAPEDATA = mgr.getShape("wait", Hyprcursor::SCursorStyleInfo{.size = 48});
if (SHAPEDATA.images.empty()) if (SHAPEDATA.images.empty()) {
std::cout << "no images\n";
return 1; return 1;
}
std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n";
// save to disk // save to disk
const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png"); const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");