diff --git a/CMakeLists.txt b/CMakeLists.txt index c0d154a..a20bfa1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY) set(CMAKE_CXX_STANDARD 23) find_package(PkgConfig REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip cairo) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.0 libzip cairo librsvg-2.0) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) message(STATUS "Configuring hyprcursor in Debug") diff --git a/README.md b/README.md index 58f889e..dd2c00e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See `docs/`. Library: - [x] Support animated cursors - - [ ] Support SVG cursors + - [x] Support SVG cursors Util: - [ ] Support compiling a theme with X @@ -54,6 +54,7 @@ Util: - hyprlang >= 0.4.2 - cairo - libzip + - librsvg ### Build ```sh diff --git a/docs/MAKING_THEMES.md b/docs/MAKING_THEMES.md index 79a8340..e554074 100644 --- a/docs/MAKING_THEMES.md +++ b/docs/MAKING_THEMES.md @@ -71,4 +71,8 @@ define_size = 32, image32.png # define_size = 64, anim4.png, 500 ``` -Supported cursor image types are png and soon svg. \ No newline at end of file +Supported cursor image types are png and svg. + +If you are using an svg cursor, the size parameter will be ignored. + +Mixing png and svg cursor images in one shape will result in an error. \ No newline at end of file diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp index 962b6cb..56bf960 100644 --- a/hyprcursor-util/src/main.cpp +++ b/hyprcursor-util/src/main.cpp @@ -141,6 +141,23 @@ static std::optional createCursorThemeFromPath(const std::string& p // check if we have at least one image. for (auto& i : SHAPE->images) { + + 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 { + std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n"; + 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"; + } + if (!std::filesystem::exists(dir.path().string() + "/" + i.filename)) return "meta invalid: image " + i.filename + " does not exist"; break; diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp index 4c31cb2..90e6a8b 100644 --- a/libhyprcursor/hyprcursor.cpp +++ b/libhyprcursor/hyprcursor.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "Log.hpp" @@ -178,7 +179,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap // matched :) bool foundAny = false; for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side != info.size) + if (image->side != info.size || (!image->artificial && shape->shapeType == SHAPE_SVG)) continue; // found size @@ -186,7 +187,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap foundAny = true; } - if (foundAny) + if (foundAny || shape->shapeType == SHAPE_SVG /* something broke, this shouldn't happen with svg */) break; // if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error. @@ -244,80 +245,122 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { - if (shape->resizeAlgo == RESIZE_NONE) + if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG) continue; // don't resample NONE style cursors bool sizeFound = false; - for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side != info.size) + if (shape->shapeType == SHAPE_PNG) { + for (auto& image : impl->loadedShapes[shape.get()].images) { + if (image->side != info.size) + continue; + + sizeFound = true; + break; + } + + if (sizeFound) continue; - - sizeFound = true; - break; } - if (sizeFound) - continue; - // size wasn't found, let's resample. - SLoadedCursorImage* leader = nullptr; - int leaderVal = 1000000; - for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side < info.size) - continue; - - if (image->side > leaderVal) - continue; - - leaderVal = image->side; - leader = image.get(); - } - - if (!leader) { + // if svg, render. + if (shape->shapeType == SHAPE_PNG) { + SLoadedCursorImage* leader = nullptr; + int leaderVal = 1000000; for (auto& image : impl->loadedShapes[shape.get()].images) { - if (std::abs((int)(image->side - info.size)) > leaderVal) + if (image->side < info.size) + continue; + + if (image->side > leaderVal) continue; leaderVal = image->side; leader = image.get(); } - } - if (!leader) { - Debug::log(ERR, "Resampling failed to find a candidate???"); + 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) { + Debug::log(ERR, "Resampling failed to find a candidate???"); + return false; + } + + auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); + 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); + + const auto PCAIRO = cairo_create(newImage->cairoSurface); + + cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); + + cairo_save(PCAIRO); + cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(PCAIRO); + cairo_restore(PCAIRO); + + const auto PTN = cairo_pattern_create_for_surface(leader->cairoSurface); + cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); + const float scale = info.size / (float)leader->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); + + cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); + + cairo_fill(PCAIRO); + cairo_surface_flush(newImage->cairoSurface); + + cairo_pattern_destroy(PTN); + cairo_destroy(PCAIRO); + } else if (shape->shapeType == SHAPE_SVG) { + const auto ORIGINALSVG = impl->loadedShapes[shape.get()].images[0].get(); + + auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); + 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); + + const auto PCAIRO = cairo_create(newImage->cairoSurface); + + cairo_save(PCAIRO); + cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(PCAIRO); + cairo_restore(PCAIRO); + + GError* error = nullptr; + RsvgHandle* handle = rsvg_handle_new_from_data((unsigned char*)ORIGINALSVG->data, ORIGINALSVG->dataLen, &error); + + if (!handle) { + Debug::log(ERR, "Failed reading svg: {}", error->message); + return false; + } + + RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size}; + + if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) { + Debug::log(ERR, "Failed rendering svg: {}", error->message); + return false; + } + + // done + cairo_surface_flush(newImage->cairoSurface); + cairo_destroy(PCAIRO); + } else { + Debug::log(ERR, "Invalid shapetype in loadThemeStyle"); return false; } - - auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); - 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); - - const auto PCAIRO = cairo_create(newImage->cairoSurface); - - cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); - - cairo_save(PCAIRO); - cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); - cairo_paint(PCAIRO); - cairo_restore(PCAIRO); - - const auto PTN = cairo_pattern_create_for_surface(leader->cairoSurface); - cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); - const float scale = info.size / (float)leader->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); - - cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); - - cairo_fill(PCAIRO); - cairo_surface_flush(newImage->cairoSurface); - - cairo_pattern_destroy(PTN); - cairo_destroy(PCAIRO); } return true; @@ -431,7 +474,7 @@ static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int le if (DATA->readNeedle >= DATA->dataLen) { delete[] (char*)DATA->data; DATA->data = nullptr; - Debug::log(LOG, "cairo: png read, freeing mem"); + Debug::log(TRACE, "cairo: png read, freeing mem"); } return CAIRO_STATUS_SUCCESS; @@ -510,8 +553,24 @@ std::optional CHyprcursorImplementation::loadTheme() { delete[] buffer; for (auto& i : SHAPE->images) { + 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 { + std::cout << "WARNING: image " << i.filename << " has no known extension, assuming png.\n"; + 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"; + } + // load image - Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); + Debug::log(TRACE, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique()).get(); IMAGE->side = i.size; @@ -526,16 +585,21 @@ std::optional CHyprcursorImplementation::loadTheme() { zip_fclose(image_file); - Debug::log(LOG, "Cairo: set up surface read"); + Debug::log(TRACE, "Cairo: set up surface read"); - IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); + if (SHAPE->shapeType == SHAPE_PNG) { - IMAGE->delay = i.delay; + IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); - if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { - delete[] (char*)IMAGE->data; - IMAGE->data = nullptr; - return "Failed reading cairoSurface, status " + std::to_string((int)STATUS); + IMAGE->delay = i.delay; + + if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { + delete[] (char*)IMAGE->data; + IMAGE->data = nullptr; + return "Failed reading cairoSurface, status " + std::to_string((int)STATUS); + } + } else { + Debug::log(LOG, "Skipping cairo load for a svg surface"); } } diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp index 0ef18a0..7a6e823 100644 --- a/libhyprcursor/internalDefines.hpp +++ b/libhyprcursor/internalDefines.hpp @@ -20,6 +20,7 @@ struct SLoadedCursorImage { size_t readNeedle = 0; void* data = nullptr; size_t dataLen = 0; + bool isSVG = false; // if true, data is just a string of chars cairo_surface_t* cairoSurface = nullptr; int side = 0; diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp index a1e8f12..baf5364 100644 --- a/libhyprcursor/internalSharedTypes.hpp +++ b/libhyprcursor/internalSharedTypes.hpp @@ -9,6 +9,12 @@ enum eResizeAlgo { RESIZE_NEAREST = 2, }; +enum eShapeType { + SHAPE_INVALID = 0, + SHAPE_PNG, + SHAPE_SVG, +}; + inline eResizeAlgo stringToAlgo(const std::string& s) { if (s == "none") return RESIZE_NONE; @@ -29,6 +35,7 @@ struct SCursorShape { eResizeAlgo resizeAlgo = RESIZE_NEAREST; std::vector images; std::vector overrides; + eShapeType shapeType = SHAPE_INVALID; }; struct SCursorTheme { diff --git a/tests/test.cpp b/tests/test.cpp index c9592d5..9fd711a 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -11,7 +11,7 @@ int main(int argc, char** argv) { } // get cursor for left_ptr - const auto SHAPEDATA = mgr.getShape("wait", Hyprcursor::SCursorStyleInfo{.size = 48}); + const auto SHAPEDATA = mgr.getShape("left_ptr", Hyprcursor::SCursorStyleInfo{.size = 48}); if (SHAPEDATA.images.empty()) { std::cout << "no images\n";