From 75614597702abcfb81f230e53ada1c8883ebaa49 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:16:55 +0100 Subject: [PATCH] lib: Added a raw data API (#23) * raw data API * add missing type arg * tests: better stuff --- CMakeLists.txt | 15 +++-- hyprcursor-util/src/main.cpp | 4 +- include/hyprcursor/hyprcursor.h | 17 ++++++ include/hyprcursor/hyprcursor.hpp | 55 ++++++++++++++++++ include/hyprcursor/shared.h | 33 +++++++++++ libhyprcursor/hyprcursor.cpp | 66 +++++++++++++++++---- libhyprcursor/hyprcursor_c.cpp | 16 ++++++ libhyprcursor/internalDefines.hpp | 2 +- libhyprcursor/internalSharedTypes.hpp | 26 ++++----- tests/{test.c => c_test.c} | 27 +++++++-- tests/{test.cpp => full_rendering.cpp} | 43 +++++++++++--- tests/only_metadata.cpp | 80 ++++++++++++++++++++++++++ 12 files changed, 337 insertions(+), 47 deletions(-) rename tests/{test.c => c_test.c} (70%) rename tests/{test.cpp => full_rendering.cpp} (52%) create mode 100644 tests/only_metadata.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2508d5f..f34b5d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,12 +65,17 @@ target_link_libraries(hyprcursor-util PkgConfig::deps hyprcursor) # tests add_custom_target(tests) -add_executable(hyprcursor_test "tests/test.cpp") -target_link_libraries(hyprcursor_test PRIVATE hyprcursor) -add_test(NAME "Test libhyprcursor in C++" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test) -add_dependencies(tests hyprcursor_test) +add_executable(hyprcursor_test1 "tests/full_rendering.cpp") +target_link_libraries(hyprcursor_test1 PRIVATE hyprcursor) +add_test(NAME "Test libhyprcursor in C++ (full rendering)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test1) +add_dependencies(tests hyprcursor_test1) -add_executable(hyprcursor_test_c "tests/test.c") +add_executable(hyprcursor_test2 "tests/only_metadata.cpp") +target_link_libraries(hyprcursor_test2 PRIVATE hyprcursor) +add_test(NAME "Test libhyprcursor in C++ (only metadata)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test2) +add_dependencies(tests hyprcursor_test2) + +add_executable(hyprcursor_test_c "tests/c_test.c") target_link_libraries(hyprcursor_test_c PRIVATE hyprcursor) add_test(NAME "Test libhyprcursor in C" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test_c) add_dependencies(tests hyprcursor_test_c) diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp index 56ba21c..4be0754 100644 --- a/hyprcursor-util/src/main.cpp +++ b/hyprcursor-util/src/main.cpp @@ -15,7 +15,7 @@ enum eOperation { OPERATION_EXTRACT = 1, }; -eResizeAlgo explicitResizeAlgo = RESIZE_INVALID; +eHyprcursorResizeAlgo explicitResizeAlgo = HC_RESIZE_INVALID; struct XCursorConfigEntry { int size = 0, hotspotX = 0, hotspotY = 0, delay = 0; @@ -329,7 +329,7 @@ static std::optional extractXTheme(const std::string& xpath_, const } // write a meta.hl - std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo)); + std::string metaString = std::format("resize_algorithm = {}\n", explicitResizeAlgo == HC_RESIZE_INVALID ? "none" : algoToString(explicitResizeAlgo)); // find hotspot from first entry metaString += diff --git a/include/hyprcursor/hyprcursor.h b/include/hyprcursor/hyprcursor.h index dc894fa..b82ea45 100644 --- a/include/hyprcursor/hyprcursor.h +++ b/include/hyprcursor/hyprcursor.h @@ -102,4 +102,21 @@ CAPI void hyprcursor_style_done(struct hyprcursor_manager_t* manager, struct hyp */ CAPI void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, PHYPRCURSORLOGFUNC fn); +/*! + \since 0.1.6 + + Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata. + + The object needs to be freed instantly after using, see hyprcursor_raw_shape_data_free() +*/ +CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape); + +/*! + \since 0.1.6 + + See hyprcursor_get_raw_shape_data. + Frees the returned object. +*/ +CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data); + #endif \ No newline at end of file diff --git a/include/hyprcursor/hyprcursor.hpp b/include/hyprcursor/hyprcursor.hpp index 6c38538..0e0f280 100644 --- a/include/hyprcursor/hyprcursor.hpp +++ b/include/hyprcursor/hyprcursor.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "shared.h" @@ -28,6 +29,24 @@ namespace Hyprcursor { std::vector images; }; + /*! + C++ structs for hyprcursor_cursor_raw_shape_image and hyprcursor_cursor_raw_shape_data + */ + struct SCursorRawShapeImage { + std::vector data; + int size = 0; + int delay = 200; + }; + + struct SCursorRawShapeData { + std::vector images; + float hotspotX = 0; + float hotspotY = 0; + std::string overridenBy = ""; + eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NONE; + eHyprcursorDataType type = HC_DATA_PNG; + }; + /*! Basic Hyprcursor manager. @@ -93,11 +112,47 @@ namespace Hyprcursor { return data; } + /*! + \since 0.1.6 + + Returns the raw image data of a cursor shape, not rendered at all, alongside the metadata. + */ + SCursorRawShapeData getRawShapeData(const char* shape_) { + auto CDATA = getRawShapeDataC(shape_); + + if (CDATA->overridenBy) { + SCursorRawShapeData d{.overridenBy = CDATA->overridenBy}; + free(CDATA->overridenBy); + delete CDATA; + return d; + } + + SCursorRawShapeData data{.hotspotX = CDATA->hotspotX, .hotspotY = CDATA->hotspotY, .overridenBy = "", .resizeAlgo = CDATA->resizeAlgo, .type = CDATA->type}; + + for (size_t i = 0; i < CDATA->len; ++i) { + SCursorRawShapeImageC* cimage = &CDATA->images[i]; + SCursorRawShapeImage& img = data.images.emplace_back(); + img.size = cimage->size; + img.delay = cimage->delay; + img.data = std::vector{(unsigned char*)cimage->data, (unsigned char*)cimage->data + (std::size_t)cimage->len}; + } + + delete[] CDATA->images; + delete CDATA; + + return data; + } + /*! Prefer getShape, this is for C compat. */ SCursorImageData** getShapesC(int& outSize, const char* shape_, const SCursorStyleInfo& info); + /*! + Prefer getShapeData, this is for C compat. + */ + SCursorRawShapeDataC* getRawShapeDataC(const char* shape_); + /*! Marks a certain style as done, allowing it to be potentially freed */ diff --git a/include/hyprcursor/shared.h b/include/hyprcursor/shared.h index 6e07a05..c3fb9c6 100644 --- a/include/hyprcursor/shared.h +++ b/include/hyprcursor/shared.h @@ -25,6 +25,39 @@ enum eHyprcursorLogLevel { HC_LOG_CRITICAL, }; +enum eHyprcursorDataType { + HC_DATA_PNG = 0, + HC_DATA_SVG, +}; + +enum eHyprcursorResizeAlgo { + HC_RESIZE_INVALID = 0, + HC_RESIZE_NONE, + HC_RESIZE_BILINEAR, + HC_RESIZE_NEAREST, +}; + +struct SCursorRawShapeImageC { + void* data; + unsigned long int len; + int size; + int delay; +}; + +typedef struct SCursorRawShapeImageC hyprcursor_cursor_raw_shape_image; + +struct SCursorRawShapeDataC { + struct SCursorRawShapeImageC* images; + unsigned long int len; + float hotspotX; + float hotspotY; + char* overridenBy; + enum eHyprcursorResizeAlgo resizeAlgo; + enum eHyprcursorDataType type; +}; + +typedef struct SCursorRawShapeDataC hyprcursor_cursor_raw_shape_data; + /* msg is owned by the caller and will be freed afterwards. */ diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp index df1cda4..11b145b 100644 --- a/libhyprcursor/hyprcursor.cpp +++ b/libhyprcursor/hyprcursor.cpp @@ -294,7 +294,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap break; // 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 != HC_RESIZE_NONE) { Debug::log(HC_LOG_ERR, logFn, "getSurfaceFor didn't match a size?"); return nullptr; } @@ -348,11 +348,59 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap return data; } +SCursorRawShapeDataC* CHyprcursorManager::getRawShapeDataC(const char* shape_) { + if (!shape_) { + Debug::log(HC_LOG_ERR, logFn, "getShapeDataC: shape of nullptr is invalid"); + return nullptr; + } + + const std::string SHAPE = shape_; + + SCursorRawShapeDataC* data = new SCursorRawShapeDataC; + std::vector resultingImages; + + for (auto& shape : impl->theme.shapes) { + // if it's overridden just return the override + if (const auto IT = std::find(shape->overrides.begin(), shape->overrides.end(), SHAPE); IT != shape->overrides.end()) { + data->overridenBy = strdup(IT->c_str()); + return data; + } + + if (shape->directory != SHAPE) + continue; + + if (!impl->loadedShapes.contains(shape.get())) + continue; // ?? + + // found it + for (auto& i : impl->loadedShapes[shape.get()].images) { + resultingImages.push_back(i.get()); + } + + data->hotspotX = shape->hotspotX; + data->hotspotY = shape->hotspotY; + data->type = shape->shapeType == SHAPE_PNG ? HC_DATA_PNG : HC_DATA_SVG; + break; + } + + data->len = resultingImages.size(); + data->images = new SCursorRawShapeImageC[data->len]; + + for (size_t i = 0; i < data->len; ++i) { + data->images[i].data = resultingImages[i]->data; + data->images[i].len = resultingImages[i]->dataLen; + data->images[i].size = resultingImages[i]->side; + data->images[i].delay = resultingImages[i]->delay; + } + + return data; +} + bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { Debug::log(HC_LOG_INFO, logFn, "loadThemeStyle: loading for size {}", info.size); for (auto& shape : impl->theme.shapes) { - if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG) { + if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) { // don't resample NONE style cursors Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: ignoring {}", shape->directory); continue; @@ -415,7 +463,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { 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 == HC_RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); cairo_save(PCAIRO); cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); @@ -426,7 +474,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { 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_pattern_set_filter(PTN, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); cairo_set_source(PCAIRO, PTN); cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); @@ -487,7 +535,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { - if (shape->resizeAlgo == RESIZE_NONE && shape->shapeType != SHAPE_SVG) + if (shape->resizeAlgo == HC_RESIZE_NONE && shape->shapeType != SHAPE_SVG) continue; std::erase_if(impl->loadedShapes[shape.get()].images, [info, &shape](const auto& e) { @@ -520,6 +568,9 @@ PNG reading static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int len) { const auto DATA = (SLoadedCursorImage*)data; + if (DATA->readNeedle >= DATA->dataLen) + return CAIRO_STATUS_READ_ERROR; + if (!DATA->data) return CAIRO_STATUS_READ_ERROR; @@ -528,11 +579,6 @@ static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int le std::memcpy(output, (uint8_t*)DATA->data + DATA->readNeedle, toRead); DATA->readNeedle += toRead; - if (DATA->readNeedle >= DATA->dataLen) { - delete[] (char*)DATA->data; - DATA->data = nullptr; - } - return CAIRO_STATUS_SUCCESS; } diff --git a/libhyprcursor/hyprcursor_c.cpp b/libhyprcursor/hyprcursor_c.cpp index b0c5499..764031d 100644 --- a/libhyprcursor/hyprcursor_c.cpp +++ b/libhyprcursor/hyprcursor_c.cpp @@ -56,3 +56,19 @@ void hyprcursor_register_logging_function(struct hyprcursor_manager_t* manager, const auto MGR = (CHyprcursorManager*)manager; MGR->registerLoggingFunction(fn); } + +CAPI hyprcursor_cursor_raw_shape_data* hyprcursor_get_raw_shape_data(struct hyprcursor_manager_t* manager, char* shape) { + const auto MGR = (CHyprcursorManager*)manager; + return MGR->getRawShapeDataC(shape); +} + +CAPI void hyprcursor_raw_shape_data_free(hyprcursor_cursor_raw_shape_data* data) { + if (data->overridenBy) { + free(data->overridenBy); + delete data; + return; + } + + delete[] data->images; + delete data; +} \ No newline at end of file diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp index 18fe5de..ef087e2 100644 --- a/libhyprcursor/internalDefines.hpp +++ b/libhyprcursor/internalDefines.hpp @@ -18,7 +18,7 @@ struct SLoadedCursorImage { // read stuff size_t readNeedle = 0; - void* data = nullptr; + void* data = nullptr; // raw png / svg data, not image data size_t dataLen = 0; bool isSVG = false; // if true, data is just a string of chars diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp index c5ac601..3a032aa 100644 --- a/libhyprcursor/internalSharedTypes.hpp +++ b/libhyprcursor/internalSharedTypes.hpp @@ -2,13 +2,7 @@ #include #include #include - -enum eResizeAlgo { - RESIZE_INVALID = 0, - RESIZE_NONE, - RESIZE_BILINEAR, - RESIZE_NEAREST, -}; +#include enum eShapeType { SHAPE_INVALID = 0, @@ -16,19 +10,19 @@ enum eShapeType { SHAPE_SVG, }; -inline eResizeAlgo stringToAlgo(const std::string& s) { +inline eHyprcursorResizeAlgo stringToAlgo(const std::string& s) { if (s == "none") - return RESIZE_NONE; + return HC_RESIZE_NONE; if (s == "nearest") - return RESIZE_NEAREST; - return RESIZE_BILINEAR; + return HC_RESIZE_NEAREST; + return HC_RESIZE_BILINEAR; } -inline std::string algoToString(const eResizeAlgo a) { +inline std::string algoToString(const eHyprcursorResizeAlgo a) { switch (a) { - case RESIZE_BILINEAR: return "bilinear"; - case RESIZE_NEAREST: return "nearest"; - case RESIZE_NONE: return "none"; + case HC_RESIZE_BILINEAR: return "bilinear"; + case HC_RESIZE_NEAREST: return "nearest"; + case HC_RESIZE_NONE: return "none"; default: return "none"; } @@ -44,7 +38,7 @@ struct SCursorImage { struct SCursorShape { std::string directory; float hotspotX = 0, hotspotY = 0; - eResizeAlgo resizeAlgo = RESIZE_NEAREST; + eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST; std::vector images; std::vector overrides; eShapeType shapeType = SHAPE_INVALID; diff --git a/tests/test.c b/tests/c_test.c similarity index 70% rename from tests/test.c rename to tests/c_test.c index 6f17443..7561fd0 100644 --- a/tests/test.c +++ b/tests/c_test.c @@ -1,3 +1,10 @@ +/* + hyprlang-test in C. + Renders a cursor shape to /tmp at 48px + + For better explanations, see the cpp tests. +*/ + #include #include #include @@ -6,11 +13,6 @@ void logFunction(enum eHyprcursorLogLevel level, char* message) { printf("[hc] %s\n", message); } -/* - hyprlang-test in C. - Renders a cursor shape to /tmp at 48px -*/ - int main(int argc, char** argv) { struct hyprcursor_manager_t* mgr = hyprcursor_manager_create_with_logger(NULL, logFunction); @@ -24,6 +26,21 @@ int main(int argc, char** argv) { return 1; } + hyprcursor_cursor_raw_shape_data* shapeData = hyprcursor_get_raw_shape_data(mgr, "left_ptr"); + if (!shapeData || shapeData->len <= 0) { + printf("failed querying left_ptr\n"); + return 1; + } + + printf("left_ptr images: %d\n", shapeData->len); + + for (size_t i = 0; i < shapeData->len; ++i) { + printf("left_ptr image size: %d\n", shapeData->images[i].len); + } + + hyprcursor_raw_shape_data_free(shapeData); + shapeData = NULL; + struct hyprcursor_cursor_style_info info = {.size = 48}; if (!hyprcursor_load_theme_style(mgr, info)) { printf("load failed\n"); diff --git a/tests/test.cpp b/tests/full_rendering.cpp similarity index 52% rename from tests/test.cpp rename to tests/full_rendering.cpp index 0ba938c..b784ea1 100644 --- a/tests/test.cpp +++ b/tests/full_rendering.cpp @@ -1,3 +1,11 @@ + +/* + full_rendering.cpp + + This example shows probably what you want to do. + Hyprcursor will render a left_ptr shape at 48x48px to a file called /tmp/arrow.png +*/ + #include #include @@ -5,29 +13,43 @@ void logFunction(enum eHyprcursorLogLevel level, char* message) { std::cout << "[hc] " << message << "\n"; } -/* - hyprlang-test in C++. - Renders a cursor shape to /tmp at 48px -*/ - int main(int argc, char** argv) { + /* + Create a manager. You can optionally pass a logger function. + */ Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction); + /* + Manager could be invalid if no themes were found, or + a specified theme was invalid. + */ if (!mgr.valid()) { std::cout << "mgr is invalid\n"; return 1; } + /* + Style describes what pixel size you want your cursor + images to be. + + Remember to free styles once you're done with them + (e.g. the user requested to change the cursor size to something else) + */ Hyprcursor::SCursorStyleInfo style{.size = 48}; - // preload size 48 for testing if (!mgr.loadThemeStyle(style)) { std::cout << "failed loading style\n"; return 1; } - // get cursor for left_ptr + /* + Get a shape. This will return the data about available image(s), + their delay, hotspot, etc. + */ const auto SHAPEDATA = mgr.getShape("left_ptr", style); + /* + If the size doesn't exist, images will be empty. + */ if (SHAPEDATA.images.empty()) { std::cout << "no images\n"; return 1; @@ -35,11 +57,16 @@ int main(int argc, char** argv) { std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n"; - // save to disk + /* + Save to disk with cairo + */ const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png"); std::cout << "Cairo returned for write: " << RET << "\n"; + /* + As mentioned before, clean up by releasing the style. + */ mgr.cursorSurfaceStyleDone(style); if (RET) diff --git a/tests/only_metadata.cpp b/tests/only_metadata.cpp new file mode 100644 index 0000000..2f73c77 --- /dev/null +++ b/tests/only_metadata.cpp @@ -0,0 +1,80 @@ + +/* + only_metadata.cpp + + This is a mode in which you probably do NOT want to operate, + but major DEs might want their own renderer for + cursor shapes. + + Prefer full_rendering.cpp for consistency and simplicity. +*/ + +#include +#include + +void logFunction(enum eHyprcursorLogLevel level, char* message) { + std::cout << "[hc] " << message << "\n"; +} + +int main(int argc, char** argv) { + /* + Create a manager. You can optionally pass a logger function. + */ + Hyprcursor::CHyprcursorManager mgr(nullptr, logFunction); + + /* + Manager could be invalid if no themes were found, or + a specified theme was invalid. + */ + if (!mgr.valid()) { + std::cout << "mgr is invalid\n"; + return 1; + } + + /* + If you are planning on using your own renderer, + you do not want to load in any styles, as those + are rendered once you make your call. + + Instead, let's request left_ptr's metadata + */ + auto RAWDATA = mgr.getRawShapeData("left_ptr"); + + /* + if images are empty, check overridenBy + */ + if (RAWDATA.images.empty()) { + + /* + if overridenBy is empty, the current theme doesn't have this shape. + */ + if (RAWDATA.overridenBy.empty()) + return false; + + /* + load what it's overriden by. + */ + RAWDATA = mgr.getRawShapeData(RAWDATA.overridenBy.c_str()); + } + + /* + If we still have no images, the theme seems broken. + */ + if (RAWDATA.images.empty()) { + std::cout << "failed querying left_ptr\n"; + return 1; + } + + /* + You can query the images (animation frames) + or their properties. + + Every image has .data and .type for you to handle. + */ + std::cout << "left_ptr images: " << RAWDATA.images.size() << "\n"; + for (auto& i : RAWDATA.images) + std::cout << "left_ptr data size: " << i.data.size() << "\n"; + + + return 0; +} \ No newline at end of file