widgets: Reload backgrounds and not just images (#583)

* widgets: Reload background and fade

* clang-format

also clang-format

* undo possibly unwanted format on Renderer.hpp

* rename stuff + style

* codestyle + initialize reloadTime

* remove trailing eols
This commit is contained in:
Robin Carlier 2024-12-18 16:28:05 +01:00 committed by GitHub
parent 381a284b3b
commit 058830668e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 365 additions and 30 deletions

View file

@ -191,6 +191,9 @@ void CConfigManager::init() {
m_config.addSpecialConfigValue("background", "vibrancy", Hyprlang::FLOAT{0.1686}); m_config.addSpecialConfigValue("background", "vibrancy", Hyprlang::FLOAT{0.1686});
m_config.addSpecialConfigValue("background", "vibrancy_darkness", Hyprlang::FLOAT{0.05}); m_config.addSpecialConfigValue("background", "vibrancy_darkness", Hyprlang::FLOAT{0.05});
m_config.addSpecialConfigValue("background", "zindex", Hyprlang::INT{-1}); m_config.addSpecialConfigValue("background", "zindex", Hyprlang::INT{-1});
m_config.addSpecialConfigValue("background", "reload_time", Hyprlang::INT{-1});
m_config.addSpecialConfigValue("background", "reload_cmd", Hyprlang::STRING{""});
m_config.addSpecialConfigValue("background", "crossfade_time", Hyprlang::FLOAT{-1.0});
m_config.addSpecialCategory("shape", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); m_config.addSpecialCategory("shape", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true});
m_config.addSpecialConfigValue("shape", "monitor", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("shape", "monitor", Hyprlang::STRING{""});
@ -318,6 +321,9 @@ std::vector<CConfigManager::SWidgetConfig> CConfigManager::getWidgetConfigs() {
{"brightness", m_config.getSpecialConfigValue("background", "brightness", k.c_str())}, {"brightness", m_config.getSpecialConfigValue("background", "brightness", k.c_str())},
{"vibrancy_darkness", m_config.getSpecialConfigValue("background", "vibrancy_darkness", k.c_str())}, {"vibrancy_darkness", m_config.getSpecialConfigValue("background", "vibrancy_darkness", k.c_str())},
{"zindex", m_config.getSpecialConfigValue("background", "zindex", k.c_str())}, {"zindex", m_config.getSpecialConfigValue("background", "zindex", k.c_str())},
{"reload_time", m_config.getSpecialConfigValue("background", "reload_time", k.c_str())},
{"reload_cmd", m_config.getSpecialConfigValue("background", "reload_cmd", k.c_str())},
{"crossfade_time", m_config.getSpecialConfigValue("background", "crossfade_time", k.c_str())},
} }
}); });
// clang-format on // clang-format on

View file

@ -106,6 +106,27 @@ CRenderer::CRenderer() {
texShader.tint = glGetUniformLocation(prog, "tint"); texShader.tint = glGetUniformLocation(prog, "tint");
texShader.useAlphaMatte = glGetUniformLocation(prog, "useAlphaMatte"); texShader.useAlphaMatte = glGetUniformLocation(prog, "useAlphaMatte");
prog = createProgram(TEXVERTSRC, TEXMIXFRAGSRCRGBA);
texMixShader.program = prog;
texMixShader.proj = glGetUniformLocation(prog, "proj");
texMixShader.tex = glGetUniformLocation(prog, "tex1");
texMixShader.tex2 = glGetUniformLocation(prog, "tex2");
texMixShader.alphaMatte = glGetUniformLocation(prog, "texMatte");
texMixShader.alpha = glGetUniformLocation(prog, "alpha");
texMixShader.mixFactor = glGetUniformLocation(prog, "mixFactor");
texMixShader.texAttrib = glGetAttribLocation(prog, "texcoord");
texMixShader.matteTexAttrib = glGetAttribLocation(prog, "texcoordMatte");
texMixShader.posAttrib = glGetAttribLocation(prog, "pos");
texMixShader.discardOpaque = glGetUniformLocation(prog, "discardOpaque");
texMixShader.discardAlpha = glGetUniformLocation(prog, "discardAlpha");
texMixShader.discardAlphaValue = glGetUniformLocation(prog, "discardAlphaValue");
texMixShader.topLeft = glGetUniformLocation(prog, "topLeft");
texMixShader.fullSize = glGetUniformLocation(prog, "fullSize");
texMixShader.radius = glGetUniformLocation(prog, "radius");
texMixShader.applyTint = glGetUniformLocation(prog, "applyTint");
texMixShader.tint = glGetUniformLocation(prog, "tint");
texMixShader.useAlphaMatte = glGetUniformLocation(prog, "useAlphaMatte");
prog = createProgram(TEXVERTSRC, FRAGBLUR1); prog = createProgram(TEXVERTSRC, FRAGBLUR1);
blurShader1.program = prog; blurShader1.program = prog;
blurShader1.tex = glGetUniformLocation(prog, "tex"); blurShader1.tex = glGetUniformLocation(prog, "tex");
@ -347,6 +368,52 @@ void CRenderer::renderTexture(const CBox& box, const CTexture& tex, float a, int
glBindTexture(tex.m_iTarget, 0); glBindTexture(tex.m_iTarget, 0);
} }
void CRenderer::renderTextureMix(const CBox& box, const CTexture& tex, const CTexture& tex2, float a, float mixFactor, int rounding, std::optional<eTransform> tr) {
const auto ROUNDEDBOX = box.copy().round();
Mat3x3 matrix = projMatrix.projectBox(ROUNDEDBOX, tr.value_or(HYPRUTILS_TRANSFORM_FLIPPED_180), box.rot);
Mat3x3 glMatrix = projection.copy().multiply(matrix);
CShader* shader = &texMixShader;
glActiveTexture(GL_TEXTURE0);
glBindTexture(tex.m_iTarget, tex.m_iTexID);
glActiveTexture(GL_TEXTURE1);
glBindTexture(tex2.m_iTarget, tex2.m_iTexID);
glUseProgram(shader->program);
glUniformMatrix3fv(shader->proj, 1, GL_TRUE, glMatrix.getMatrix().data());
glUniform1i(shader->tex, 0);
glUniform1i(shader->tex2, 1);
glUniform1f(shader->alpha, a);
glUniform1f(shader->mixFactor, mixFactor);
const auto TOPLEFT = Vector2D(ROUNDEDBOX.x, ROUNDEDBOX.y);
const auto FULLSIZE = Vector2D(ROUNDEDBOX.width, ROUNDEDBOX.height);
// Rounded corners
glUniform2f(shader->topLeft, TOPLEFT.x, TOPLEFT.y);
glUniform2f(shader->fullSize, FULLSIZE.x, FULLSIZE.y);
glUniform1f(shader->radius, rounding);
glUniform1i(shader->discardOpaque, 0);
glUniform1i(shader->discardAlpha, 0);
glUniform1i(shader->applyTint, 0);
glVertexAttribPointer(shader->posAttrib, 2, GL_FLOAT, GL_FALSE, 0, fullVerts);
glVertexAttribPointer(shader->texAttrib, 2, GL_FLOAT, GL_FALSE, 0, fullVerts);
glEnableVertexAttribArray(shader->posAttrib);
glEnableVertexAttribArray(shader->texAttrib);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableVertexAttribArray(shader->posAttrib);
glDisableVertexAttribArray(shader->texAttrib);
glBindTexture(tex.m_iTarget, 0);
}
std::vector<std::unique_ptr<IWidget>>* CRenderer::getOrCreateWidgetsFor(const CSessionLockSurface* surf) { std::vector<std::unique_ptr<IWidget>>* CRenderer::getOrCreateWidgetsFor(const CSessionLockSurface* surf) {
if (!widgets.contains(surf)) { if (!widgets.contains(surf)) {

View file

@ -33,6 +33,7 @@ class CRenderer {
void renderRect(const CBox& box, const CColor& col, int rounding = 0); void renderRect(const CBox& box, const CColor& col, int rounding = 0);
void renderBorder(const CBox& box, const CGradientValueData& gradient, int thickness, int rounding = 0, float alpha = 1.0); void renderBorder(const CBox& box, const CGradientValueData& gradient, int thickness, int rounding = 0, float alpha = 1.0);
void renderTexture(const CBox& box, const CTexture& tex, float a = 1.0, int rounding = 0, std::optional<eTransform> tr = {}); void renderTexture(const CBox& box, const CTexture& tex, float a = 1.0, int rounding = 0, std::optional<eTransform> tr = {});
void renderTextureMix(const CBox& box, const CTexture& tex, const CTexture& tex2, float a = 1.0, float mixFactor = 0.0, int rounding = 0, std::optional<eTransform> tr = {});
void blurFB(const CFramebuffer& outfb, SBlurParams params); void blurFB(const CFramebuffer& outfb, SBlurParams params);
std::unique_ptr<CAsyncResourceGatherer> asyncResourceGatherer; std::unique_ptr<CAsyncResourceGatherer> asyncResourceGatherer;
@ -50,6 +51,7 @@ class CRenderer {
CShader rectShader; CShader rectShader;
CShader texShader; CShader texShader;
CShader texMixShader;
CShader blurShader1; CShader blurShader1;
CShader blurShader2; CShader blurShader2;
CShader blurPrepareShader; CShader blurPrepareShader;

View file

@ -13,7 +13,9 @@ class CShader {
GLint color = -1; GLint color = -1;
GLint alphaMatte = -1; GLint alphaMatte = -1;
GLint tex = -1; GLint tex = -1;
GLint tex2 = -1;
GLint alpha = -1; GLint alpha = -1;
GLfloat mixFactor = -1;
GLint posAttrib = -1; GLint posAttrib = -1;
GLint texAttrib = -1; GLint texAttrib = -1;
GLint matteTexAttrib = -1; GLint matteTexAttrib = -1;

View file

@ -129,6 +129,49 @@ void main() {
gl_FragColor = pixColor * alpha; gl_FragColor = pixColor * alpha;
})#"; })#";
inline const std::string TEXMIXFRAGSRCRGBA = R"#(
precision highp float;
varying vec2 v_texcoord; // is in 0-1
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform float mixFactor;
uniform float alpha;
uniform vec2 topLeft;
uniform vec2 fullSize;
uniform float radius;
uniform int discardOpaque;
uniform int discardAlpha;
uniform float discardAlphaValue;
uniform int applyTint;
uniform vec3 tint;
void main() {
vec4 pixColor = mix(texture2D(tex1, v_texcoord), texture2D(tex2, v_texcoord), smoothstep(0.0, 1.0, mixFactor));
if (discardOpaque == 1 && pixColor[3] * alpha == 1.0)
discard;
if (discardAlpha == 1 && pixColor[3] <= discardAlphaValue)
discard;
if (applyTint == 1) {
pixColor[0] = pixColor[0] * tint[0];
pixColor[1] = pixColor[1] * tint[1];
pixColor[2] = pixColor[2] * tint[2];
}
if (radius > 0.0) {
)#" +
ROUNDED_SHADER_FUNC("pixColor") + R"#(
}
gl_FragColor = pixColor * alpha;
})#";
inline const std::string FRAGBLUR1 = R"#( inline const std::string FRAGBLUR1 = R"#(
#version 100 #version 100
precision highp float; precision highp float;

View file

@ -1,10 +1,33 @@
#include "Background.hpp" #include "Background.hpp"
#include "../Renderer.hpp" #include "../Renderer.hpp"
#include "../../core/hyprlock.hpp"
#include "src/helpers/Log.hpp"
#include <chrono>
#include <hyprlang.hpp> #include <hyprlang.hpp>
#include <filesystem>
#include <memory>
#include <GLES3/gl32.h>
CBackground::~CBackground() {
if (reloadTimer) {
reloadTimer->cancel();
reloadTimer.reset();
}
if (fade) {
if (fade->crossFadeTimer) {
fade->crossFadeTimer->cancel();
fade->crossFadeTimer.reset();
}
fade.reset();
}
}
CBackground::CBackground(const Vector2D& viewport_, COutput* output_, const std::string& resourceID_, const std::unordered_map<std::string, std::any>& props, bool ss) : CBackground::CBackground(const Vector2D& viewport_, COutput* output_, const std::string& resourceID_, const std::unordered_map<std::string, std::any>& props, bool ss) :
viewport(viewport_), resourceID(resourceID_), output(output_), isScreenshot(ss) { viewport(viewport_), resourceID(resourceID_), output(output_), isScreenshot(ss) {
try {
color = std::any_cast<Hyprlang::INT>(props.at("color")); color = std::any_cast<Hyprlang::INT>(props.at("color"));
blurPasses = std::any_cast<Hyprlang::INT>(props.at("blur_passes")); blurPasses = std::any_cast<Hyprlang::INT>(props.at("blur_passes"));
blurSize = std::any_cast<Hyprlang::INT>(props.at("blur_size")); blurSize = std::any_cast<Hyprlang::INT>(props.at("blur_size"));
@ -13,6 +36,23 @@ CBackground::CBackground(const Vector2D& viewport_, COutput* output_, const std:
noise = std::any_cast<Hyprlang::FLOAT>(props.at("noise")); noise = std::any_cast<Hyprlang::FLOAT>(props.at("noise"));
brightness = std::any_cast<Hyprlang::FLOAT>(props.at("brightness")); brightness = std::any_cast<Hyprlang::FLOAT>(props.at("brightness"));
contrast = std::any_cast<Hyprlang::FLOAT>(props.at("contrast")); contrast = std::any_cast<Hyprlang::FLOAT>(props.at("contrast"));
path = std::any_cast<Hyprlang::STRING>(props.at("path"));
reloadCommand = std::any_cast<Hyprlang::STRING>(props.at("reload_cmd"));
reloadTime = std::any_cast<Hyprlang::INT>(props.at("reload_time"));
crossFadeTime = std::any_cast<Hyprlang::FLOAT>(props.at("crossfade_time"));
} catch (const std::bad_any_cast& e) {
RASSERT(false, "Failed to construct CBackground: {}", e.what()); //
} catch (const std::out_of_range& e) {
RASSERT(false, "Missing propperty for CBackground: {}", e.what()); //
}
try {
modificationTime = std::filesystem::last_write_time(path);
} catch (std::exception& e) { Debug::log(ERR, "{}", e.what()); }
if (!isScreenshot)
plantReloadTimer(); // No reloads for screenshots.
} }
void CBackground::renderRect(CColor color) { void CBackground::renderRect(CColor color) {
@ -20,6 +60,27 @@ void CBackground::renderRect(CColor color) {
g_pRenderer->renderRect(monbox, color, 0); g_pRenderer->renderRect(monbox, color, 0);
} }
static void onReloadTimer(std::shared_ptr<CTimer> self, void* data) {
const auto PBG = (CBackground*)data;
PBG->onReloadTimerUpdate();
PBG->plantReloadTimer();
}
static void onCrossFadeTimer(std::shared_ptr<CTimer> self, void* data) {
const auto PBG = (CBackground*)data;
PBG->onCrossFadeTimerUpdate();
}
static void onAssetCallback(void* data) {
const auto PBG = (CBackground*)data;
PBG->startCrossFadeOrUpdateRender();
}
static void onAssetCallbackTimer(std::shared_ptr<CTimer> self, void* data) {
onAssetCallback(data);
}
bool CBackground::draw(const SRenderData& data) { bool CBackground::draw(const SRenderData& data) {
if (resourceID.empty()) { if (resourceID.empty()) {
@ -45,10 +106,13 @@ bool CBackground::draw(const SRenderData& data) {
return true; return true;
} }
if ((blurPasses > 0 || isScreenshot) && !blurredFB.isAllocated()) { if (fade || ((blurPasses > 0 || isScreenshot) && (!blurredFB.isAllocated() || firstRender))) {
if (firstRender)
firstRender = false;
// make it brah // make it brah
Vector2D size = asset->texture.m_vSize; Vector2D size = asset->texture.m_vSize;
if (output->transform % 2 == 1 && isScreenshot) { if (output->transform % 2 == 1 && isScreenshot) {
size.x = asset->texture.m_vSize.y; size.x = asset->texture.m_vSize.y;
size.y = asset->texture.m_vSize.x; size.y = asset->texture.m_vSize.x;
@ -67,13 +131,23 @@ bool CBackground::draw(const SRenderData& data) {
else else
texbox.x = -(texbox.w - viewport.x) / 2.f; texbox.x = -(texbox.w - viewport.x) / 2.f;
texbox.round(); texbox.round();
if (!blurredFB.isAllocated())
blurredFB.alloc(viewport.x, viewport.y); // TODO 10 bit blurredFB.alloc(viewport.x, viewport.y); // TODO 10 bit
blurredFB.bind(); blurredFB.bind();
if (fade)
g_pRenderer->renderTextureMix(texbox, asset->texture, pendingAsset->texture, 1.0,
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - fade->start).count() / (1000 * crossFadeTime),
0, HYPRUTILS_TRANSFORM_NORMAL);
else
g_pRenderer->renderTexture(texbox, asset->texture, 1.0, 0, g_pRenderer->renderTexture(texbox, asset->texture, 1.0, 0,
isScreenshot ? isScreenshot ?
wlTransformToHyprutils(invertTransform(output->transform)) : wlTransformToHyprutils(invertTransform(output->transform)) :
HYPRUTILS_TRANSFORM_NORMAL); // this could be omitted but whatever it's only once and makes code cleaner plus less blurring on large texs HYPRUTILS_TRANSFORM_NORMAL); // this could be omitted but whatever it's only once and makes code cleaner plus less blurring on large texs
if (blurPasses > 0) if (blurPasses > 0)
g_pRenderer->blurFB(blurredFB, CRenderer::SBlurParams{blurSize, blurPasses, noise, contrast, brightness, vibrancy, vibrancy_darkness}); g_pRenderer->blurFB(blurredFB, CRenderer::SBlurParams{blurSize, blurPasses, noise, contrast, brightness, vibrancy, vibrancy_darkness});
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
@ -97,5 +171,114 @@ bool CBackground::draw(const SRenderData& data) {
texbox.round(); texbox.round();
g_pRenderer->renderTexture(texbox, *tex, data.opacity, 0, HYPRUTILS_TRANSFORM_FLIPPED_180); g_pRenderer->renderTexture(texbox, *tex, data.opacity, 0, HYPRUTILS_TRANSFORM_FLIPPED_180);
return data.opacity < 1.0; return fade || data.opacity < 1.0; // actively render during fading
}
void CBackground::plantReloadTimer() {
if (reloadTime == 0)
reloadTimer = g_pHyprlock->addTimer(std::chrono::hours(1), onReloadTimer, this, true);
else if (reloadTime > 0)
reloadTimer = g_pHyprlock->addTimer(std::chrono::seconds(reloadTime), onReloadTimer, this, false);
}
void CBackground::onCrossFadeTimerUpdate() {
// Animation done: Unload previous asset, deinitialize the fade and pass the asset
if (fade) {
fade->crossFadeTimer.reset();
fade.reset(nullptr);
}
if (!(blurPasses > 0 || isScreenshot))
blurredFB.release();
asset = pendingAsset;
resourceID = pendingResourceID;
pendingResourceID = "";
pendingAsset = nullptr;
firstRender = true;
g_pHyprlock->renderOutput(output->stringPort);
}
void CBackground::onReloadTimerUpdate() {
const std::string OLDPATH = path;
// Path parsing and early returns
if (!reloadCommand.empty()) {
path = g_pHyprlock->spawnSync(reloadCommand);
if (path.ends_with('\n'))
path.pop_back();
if (path.starts_with("file://"))
path = path.substr(7);
if (path.empty())
return;
}
try {
const auto MTIME = std::filesystem::last_write_time(path);
if (OLDPATH == path && MTIME == modificationTime)
return;
modificationTime = MTIME;
} catch (std::exception& e) {
path = OLDPATH;
Debug::log(ERR, "{}", e.what());
return;
}
if (!pendingResourceID.empty())
return;
// Issue the next request
request.id = std::string{"background:"} + path + ",time:" + std::to_string((uint64_t)modificationTime.time_since_epoch().count());
pendingResourceID = request.id;
request.asset = path;
request.type = CAsyncResourceGatherer::eTargetType::TARGET_IMAGE;
request.callback = onAssetCallback;
request.callbackData = this;
g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request);
}
void CBackground::startCrossFadeOrUpdateRender() {
auto newAsset = g_pRenderer->asyncResourceGatherer->getAssetByID(pendingResourceID);
if (newAsset) {
if (newAsset->texture.m_iType == TEXTURE_INVALID) {
g_pRenderer->asyncResourceGatherer->unloadAsset(newAsset);
Debug::log(ERR, "New asset had an invalid texture!");
} else if (resourceID != pendingResourceID) {
pendingAsset = newAsset;
if (crossFadeTime > 0) {
// Start a fade
if (!fade)
fade = std::make_unique<SFade>(std::chrono::system_clock::now(), 0, nullptr);
else {
// Maybe we where already fading so reset it just in case, but should'nt be happening.
if (fade->crossFadeTimer) {
fade->crossFadeTimer->cancel();
fade->crossFadeTimer.reset();
}
}
fade->start = std::chrono::system_clock::now();
fade->a = 0;
fade->crossFadeTimer = g_pHyprlock->addTimer(std::chrono::milliseconds((int)(1000.0 * crossFadeTime)), onCrossFadeTimer, this);
} else {
onCrossFadeTimerUpdate();
}
}
} else if (!pendingResourceID.empty()) {
Debug::log(WARN, "Asset {} not available after the asyncResourceGatherer's callback!", pendingResourceID);
g_pHyprlock->addTimer(std::chrono::milliseconds(100), onAssetCallbackTimer, this);
}
g_pHyprlock->renderOutput(output->stringPort);
} }

View file

@ -3,21 +3,37 @@
#include "IWidget.hpp" #include "IWidget.hpp"
#include "../../helpers/Color.hpp" #include "../../helpers/Color.hpp"
#include "../../helpers/Math.hpp" #include "../../helpers/Math.hpp"
#include "../../core/Timer.hpp"
#include "../Framebuffer.hpp" #include "../Framebuffer.hpp"
#include "../AsyncResourceGatherer.hpp"
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <any> #include <any>
#include <chrono>
#include <filesystem>
struct SPreloadedAsset; struct SPreloadedAsset;
class COutput; class COutput;
struct SFade {
std::chrono::system_clock::time_point start;
float a = 0;
std::shared_ptr<CTimer> crossFadeTimer = nullptr;
};
class CBackground : public IWidget { class CBackground : public IWidget {
public: public:
CBackground(const Vector2D& viewport, COutput* output_, const std::string& resourceID, const std::unordered_map<std::string, std::any>& props, bool ss_); CBackground(const Vector2D& viewport, COutput* output_, const std::string& resourceID, const std::unordered_map<std::string, std::any>& props, bool ss_);
~CBackground();
virtual bool draw(const SRenderData& data); virtual bool draw(const SRenderData& data);
void renderRect(CColor color); void renderRect(CColor color);
void onReloadTimerUpdate();
void onCrossFadeTimerUpdate();
void plantReloadTimer();
void startCrossFadeOrUpdateRender();
private: private:
// if needed // if needed
CFramebuffer blurredFB; CFramebuffer blurredFB;
@ -30,9 +46,25 @@ class CBackground : public IWidget {
float vibrancy = 0.1696; float vibrancy = 0.1696;
float vibrancy_darkness = 0.0; float vibrancy_darkness = 0.0;
Vector2D viewport; Vector2D viewport;
std::string path = "";
std::string resourceID; std::string resourceID;
std::string pendingResourceID;
float crossFadeTime = -1.0;
CColor color; CColor color;
SPreloadedAsset* asset = nullptr; SPreloadedAsset* asset = nullptr;
COutput* output = nullptr; COutput* output = nullptr;
bool isScreenshot = false; bool isScreenshot = false;
SPreloadedAsset* pendingAsset = nullptr;
bool firstRender = true;
std::unique_ptr<SFade> fade;
int reloadTime = -1;
std::string reloadCommand;
CAsyncResourceGatherer::SPreloadRequest request;
std::shared_ptr<CTimer> reloadTimer;
std::filesystem::file_time_type modificationTime;
}; };