mirror of
https://github.com/hyprwm/hyprcursor.git
synced 2024-11-16 18:25:58 +01:00
core: support svg cursors
This commit is contained in:
parent
14faee7ed1
commit
74ae8a1847
8 changed files with 166 additions and 72 deletions
|
@ -19,7 +19,7 @@ configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY)
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
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)
|
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
|
||||||
message(STATUS "Configuring hyprcursor in Debug")
|
message(STATUS "Configuring hyprcursor in Debug")
|
||||||
|
|
|
@ -42,7 +42,7 @@ See `docs/`.
|
||||||
|
|
||||||
Library:
|
Library:
|
||||||
- [x] Support animated cursors
|
- [x] Support animated cursors
|
||||||
- [ ] Support SVG cursors
|
- [x] Support SVG cursors
|
||||||
|
|
||||||
Util:
|
Util:
|
||||||
- [ ] Support compiling a theme with X
|
- [ ] Support compiling a theme with X
|
||||||
|
@ -54,6 +54,7 @@ Util:
|
||||||
- hyprlang >= 0.4.2
|
- hyprlang >= 0.4.2
|
||||||
- cairo
|
- cairo
|
||||||
- libzip
|
- libzip
|
||||||
|
- librsvg
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -71,4 +71,8 @@ define_size = 32, image32.png
|
||||||
# define_size = 64, anim4.png, 500
|
# define_size = 64, anim4.png, 500
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported cursor image types are png and soon svg.
|
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.
|
|
@ -141,6 +141,23 @@ static std::optional<std::string> createCursorThemeFromPath(const std::string& p
|
||||||
|
|
||||||
// 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 (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))
|
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;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <zip.h>
|
#include <zip.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <librsvg/rsvg.h>
|
||||||
|
|
||||||
#include "Log.hpp"
|
#include "Log.hpp"
|
||||||
|
|
||||||
|
@ -178,7 +179,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
|
||||||
// matched :)
|
// matched :)
|
||||||
bool foundAny = false;
|
bool foundAny = false;
|
||||||
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
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;
|
continue;
|
||||||
|
|
||||||
// found size
|
// found size
|
||||||
|
@ -186,7 +187,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap
|
||||||
foundAny = true;
|
foundAny = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundAny)
|
if (foundAny || shape->shapeType == SHAPE_SVG /* something broke, this shouldn't happen with svg */)
|
||||||
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.
|
||||||
|
@ -244,80 +245,122 @@ 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 && shape->shapeType != SHAPE_SVG)
|
||||||
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.get()].images) {
|
if (shape->shapeType == SHAPE_PNG) {
|
||||||
if (image->side != info.size)
|
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
||||||
|
if (image->side != info.size)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sizeFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeFound)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
sizeFound = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizeFound)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// size wasn't found, let's resample.
|
// size wasn't found, let's resample.
|
||||||
SLoadedCursorImage* leader = nullptr;
|
// if svg, render.
|
||||||
int leaderVal = 1000000;
|
if (shape->shapeType == SHAPE_PNG) {
|
||||||
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
SLoadedCursorImage* leader = nullptr;
|
||||||
if (image->side < info.size)
|
int leaderVal = 1000000;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (image->side > leaderVal)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
leaderVal = image->side;
|
|
||||||
leader = image.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!leader) {
|
|
||||||
for (auto& image : impl->loadedShapes[shape.get()].images) {
|
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;
|
continue;
|
||||||
|
|
||||||
leaderVal = image->side;
|
leaderVal = image->side;
|
||||||
leader = image.get();
|
leader = image.get();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!leader) {
|
if (!leader) {
|
||||||
Debug::log(ERR, "Resampling failed to find a candidate???");
|
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<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);
|
||||||
|
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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;
|
return true;
|
||||||
|
@ -431,7 +474,7 @@ static cairo_status_t readPNG(void* data, unsigned char* output, unsigned int le
|
||||||
if (DATA->readNeedle >= DATA->dataLen) {
|
if (DATA->readNeedle >= DATA->dataLen) {
|
||||||
delete[] (char*)DATA->data;
|
delete[] (char*)DATA->data;
|
||||||
DATA->data = nullptr;
|
DATA->data = nullptr;
|
||||||
Debug::log(LOG, "cairo: png read, freeing mem");
|
Debug::log(TRACE, "cairo: png read, freeing mem");
|
||||||
}
|
}
|
||||||
|
|
||||||
return CAIRO_STATUS_SUCCESS;
|
return CAIRO_STATUS_SUCCESS;
|
||||||
|
@ -510,8 +553,24 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
|
||||||
delete[] buffer;
|
delete[] buffer;
|
||||||
|
|
||||||
for (auto& i : SHAPE->images) {
|
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
|
// 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<SLoadedCursorImage>()).get();
|
auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique<SLoadedCursorImage>()).get();
|
||||||
IMAGE->side = i.size;
|
IMAGE->side = i.size;
|
||||||
|
|
||||||
|
@ -526,16 +585,21 @@ std::optional<std::string> CHyprcursorImplementation::loadTheme() {
|
||||||
|
|
||||||
zip_fclose(image_file);
|
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) {
|
IMAGE->delay = i.delay;
|
||||||
delete[] (char*)IMAGE->data;
|
|
||||||
IMAGE->data = nullptr;
|
if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) {
|
||||||
return "Failed reading cairoSurface, status " + std::to_string((int)STATUS);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ struct SLoadedCursorImage {
|
||||||
size_t readNeedle = 0;
|
size_t readNeedle = 0;
|
||||||
void* data = nullptr;
|
void* data = nullptr;
|
||||||
size_t dataLen = 0;
|
size_t dataLen = 0;
|
||||||
|
bool isSVG = false; // if true, data is just a string of chars
|
||||||
|
|
||||||
cairo_surface_t* cairoSurface = nullptr;
|
cairo_surface_t* cairoSurface = nullptr;
|
||||||
int side = 0;
|
int side = 0;
|
||||||
|
|
|
@ -9,6 +9,12 @@ enum eResizeAlgo {
|
||||||
RESIZE_NEAREST = 2,
|
RESIZE_NEAREST = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum eShapeType {
|
||||||
|
SHAPE_INVALID = 0,
|
||||||
|
SHAPE_PNG,
|
||||||
|
SHAPE_SVG,
|
||||||
|
};
|
||||||
|
|
||||||
inline eResizeAlgo stringToAlgo(const std::string& s) {
|
inline eResizeAlgo stringToAlgo(const std::string& s) {
|
||||||
if (s == "none")
|
if (s == "none")
|
||||||
return RESIZE_NONE;
|
return RESIZE_NONE;
|
||||||
|
@ -29,6 +35,7 @@ struct SCursorShape {
|
||||||
eResizeAlgo resizeAlgo = RESIZE_NEAREST;
|
eResizeAlgo resizeAlgo = RESIZE_NEAREST;
|
||||||
std::vector<SCursorImage> images;
|
std::vector<SCursorImage> images;
|
||||||
std::vector<std::string> overrides;
|
std::vector<std::string> overrides;
|
||||||
|
eShapeType shapeType = SHAPE_INVALID;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SCursorTheme {
|
struct SCursorTheme {
|
||||||
|
|
|
@ -11,7 +11,7 @@ int main(int argc, char** argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cursor for left_ptr
|
// 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()) {
|
if (SHAPEDATA.images.empty()) {
|
||||||
std::cout << "no images\n";
|
std::cout << "no images\n";
|
||||||
|
|
Loading…
Reference in a new issue