From e207d34b88ce4d755c3ffc61ce2d9276bf663b87 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Mon, 19 Feb 2024 16:26:08 +0000 Subject: [PATCH] widgets: add label --- CMakeLists.txt | 2 +- src/config/ConfigManager.cpp | 29 +++++ src/renderer/AsyncResourceGatherer.cpp | 124 +++++++++++++++++++- src/renderer/AsyncResourceGatherer.hpp | 40 ++++++- src/renderer/Renderer.cpp | 13 +- src/renderer/widgets/Background.cpp | 9 +- src/renderer/widgets/Background.hpp | 2 +- src/renderer/widgets/IWidget.hpp | 6 +- src/renderer/widgets/Label.cpp | 81 +++++++++++++ src/renderer/widgets/Label.hpp | 26 ++++ src/renderer/widgets/PasswordInputField.cpp | 8 +- src/renderer/widgets/PasswordInputField.hpp | 2 +- 12 files changed, 319 insertions(+), 23 deletions(-) create mode 100644 src/renderer/widgets/Label.cpp create mode 100644 src/renderer/widgets/Label.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b29316..6aac750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ message(STATUS "Checking deps...") find_package(Threads REQUIRED) find_package(PkgConfig REQUIRED) find_package(OpenGL REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols wayland-egl hyprlang>=0.4.0 egl opengl xkbcommon cairo pango pam) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols wayland-egl hyprlang>=0.4.0 egl opengl xkbcommon cairo pangocairo pam) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") add_executable(hyprlock ${SRCFILES}) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b5e936a..102a2f5 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -33,6 +33,16 @@ void CConfigManager::init() { m_config.addSpecialConfigValue("input-field", "outline_thickness", Hyprlang::INT{4}); m_config.addSpecialConfigValue("input-field", "fade_on_empty", Hyprlang::INT{1}); + m_config.addSpecialCategory("label", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); + m_config.addSpecialConfigValue("label", "monitor", Hyprlang::STRING{""}); + m_config.addSpecialConfigValue("label", "position", Hyprlang::VEC2{400, 90}); + m_config.addSpecialConfigValue("label", "color", Hyprlang::INT{0xFFFFFFFF}); + m_config.addSpecialConfigValue("label", "font_size", Hyprlang::INT{16}); + m_config.addSpecialConfigValue("label", "text", Hyprlang::STRING{"Sample Text"}); + m_config.addSpecialConfigValue("label", "font_family", Hyprlang::STRING{"Sans"}); + m_config.addSpecialConfigValue("label", "halign", Hyprlang::STRING{"none"}); + m_config.addSpecialConfigValue("label", "valign", Hyprlang::STRING{"none"}); + m_config.commence(); auto result = m_config.parse(); @@ -82,5 +92,24 @@ std::vector CConfigManager::getWidgetConfigs() { // clang-format on } + keys = m_config.listKeysForSpecialCategory("label"); + for (auto& k : keys) { + // clang-format off + result.push_back(CConfigManager::SWidgetConfig{ + "label", + std::any_cast(m_config.getSpecialConfigValue("label", "monitor", k.c_str())), + { + {"position", m_config.getSpecialConfigValue("label", "position", k.c_str())}, + {"color", m_config.getSpecialConfigValue("label", "color", k.c_str())}, + {"font_size", m_config.getSpecialConfigValue("label", "font_size", k.c_str())}, + {"font_family", m_config.getSpecialConfigValue("label", "font_family", k.c_str())}, + {"text", m_config.getSpecialConfigValue("label", "text", k.c_str())}, + {"halign", m_config.getSpecialConfigValue("label", "halign", k.c_str())}, + {"valign", m_config.getSpecialConfigValue("label", "valign", k.c_str())}, + } + }); + // clang-format on + } + return result; } \ No newline at end of file diff --git a/src/renderer/AsyncResourceGatherer.cpp b/src/renderer/AsyncResourceGatherer.cpp index 8a33cde..deb7d34 100644 --- a/src/renderer/AsyncResourceGatherer.cpp +++ b/src/renderer/AsyncResourceGatherer.cpp @@ -2,19 +2,40 @@ #include "../config/ConfigManager.hpp" #include "../core/Egl.hpp" #include +#include #include +#include "../core/hyprlock.hpp" + +std::mutex cvmtx; CAsyncResourceGatherer::CAsyncResourceGatherer() { - thread = std::thread([this]() { this->gather(); }); - thread.detach(); + initThread = std::thread([this]() { + this->gather(); + this->asyncLoopThread = std::thread([this]() { this->asyncAssetSpinLock(); }); + this->asyncLoopThread.detach(); + }); + initThread.detach(); } SPreloadedAsset* CAsyncResourceGatherer::getAssetByID(const std::string& id) { + if (asyncLoopState.busy) + return nullptr; + + std::lock_guard lg(asyncLoopState.loopMutex); + for (auto& a : assets) { if (a.first == id) return &a.second; } + if (!preloadTargets.empty()) { + apply(); + for (auto& a : assets) { + if (a.first == id) + return &a.second; + } + } + return nullptr; } @@ -85,8 +106,107 @@ void CAsyncResourceGatherer::apply() { cairo_destroy((cairo_t*)t.cairo); cairo_surface_destroy((cairo_surface_t*)t.cairosurface); + } else { + Debug::log(ERR, "Unsupported type in ::apply() {}", (int)t.type); } } + preloadTargets.clear(); + applied = true; +} + +void CAsyncResourceGatherer::renderText(const SPreloadRequest& rq) { + SPreloadTarget target; + target.type = TARGET_IMAGE; /* text is just an image lol */ + target.id = rq.id; + + const int FONTSIZE = rq.props.contains("font_size") ? std::any_cast(rq.props.at("font_size")) : 16; + const CColor FONTCOLOR = rq.props.contains("color") ? std::any_cast(rq.props.at("color")) : CColor(1.0, 1.0, 1.0, 1.0); + const std::string FONTFAMILY = rq.props.contains("font_family") ? std::any_cast(rq.props.at("font_family")) : "Sans"; + + auto CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1920, 1080 /* dummy value */); + auto CAIRO = cairo_create(CAIROSURFACE); + + // draw title using Pango + PangoLayout* layout = pango_cairo_create_layout(CAIRO); + pango_layout_set_text(layout, rq.asset.c_str(), -1); + + PangoFontDescription* fontDesc = pango_font_description_from_string(FONTFAMILY.c_str()); + pango_font_description_set_size(fontDesc, FONTSIZE * PANGO_SCALE); + pango_layout_set_font_description(layout, fontDesc); + pango_font_description_free(fontDesc); + int layoutWidth, layoutHeight; + pango_layout_get_size(layout, &layoutWidth, &layoutHeight); + + // TODO: avoid this? + cairo_destroy(CAIRO); + cairo_surface_destroy(CAIROSURFACE); + CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, layoutWidth / PANGO_SCALE, layoutHeight / PANGO_SCALE); + CAIRO = cairo_create(CAIROSURFACE); + + // clear the pixmap + cairo_save(CAIRO); + cairo_set_operator(CAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(CAIRO); + cairo_restore(CAIRO); + + // render the thing + cairo_set_source_rgba(CAIRO, FONTCOLOR.r, FONTCOLOR.g, FONTCOLOR.b, FONTCOLOR.a); + + cairo_move_to(CAIRO, 0, 0); + pango_cairo_show_layout(CAIRO, layout); + + g_object_unref(layout); + + cairo_surface_flush(CAIROSURFACE); + + target.cairo = CAIRO; + target.cairosurface = CAIROSURFACE; + target.data = cairo_image_surface_get_data(CAIROSURFACE); + target.size = {layoutWidth / PANGO_SCALE, layoutHeight / PANGO_SCALE}; + + preloadTargets.push_back(target); +} + +void CAsyncResourceGatherer::asyncAssetSpinLock() { + while (!g_pHyprlock->m_bTerminate) { + + std::unique_lock lk(cvmtx); + if (asyncLoopState.pending == false) // avoid a lock if a thread managed to request something already since we .unlock()ed + asyncLoopState.loopGuard.wait_for(lk, std::chrono::seconds(5), [this] { return asyncLoopState.pending; }); // wait for events + + asyncLoopState.requestMutex.lock(); + + if (asyncLoopState.requests.empty()) { + asyncLoopState.requestMutex.unlock(); + continue; + } + + auto requests = asyncLoopState.requests; + asyncLoopState.requests.clear(); + + asyncLoopState.requestMutex.unlock(); + + // process requests + + asyncLoopState.busy = true; + for (auto& r : requests) { + if (r.type == TARGET_TEXT) { + renderText(r); + } else { + Debug::log(ERR, "Unsupported async preload type {}??", (int)r.type); + } + } + + asyncLoopState.busy = false; + } +} + +void CAsyncResourceGatherer::requestAsyncAssetPreload(const SPreloadRequest& request) { + std::lock_guard lg(asyncLoopState.requestMutex); + asyncLoopState.requests.push_back(request); + std::unique_lock lk(cvmtx); + asyncLoopState.pending = true; + asyncLoopState.loopGuard.notify_all(); } \ No newline at end of file diff --git a/src/renderer/AsyncResourceGatherer.hpp b/src/renderer/AsyncResourceGatherer.hpp index 6c0f8a0..8a9e552 100644 --- a/src/renderer/AsyncResourceGatherer.hpp +++ b/src/renderer/AsyncResourceGatherer.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include struct SPreloadedAsset { CTexture texture; @@ -21,17 +23,45 @@ class CAsyncResourceGatherer { std::atomic progress = 0; - SPreloadedAsset* getAssetByID(const std::string& id); + /* only call from ogl thread */ + SPreloadedAsset* getAssetByID(const std::string& id); - void apply(); - - private: - std::thread thread; + void apply(); enum eTargetType { TARGET_IMAGE = 0, + TARGET_TEXT }; + struct SPreloadRequest { + eTargetType type; + std::string asset; + std::string id; + + std::unordered_map props; + }; + + void requestAsyncAssetPreload(const SPreloadRequest& request); + + private: + std::thread initThread; + std::thread asyncLoopThread; + + void asyncAssetSpinLock(); + void renderText(const SPreloadRequest& rq); + + struct { + std::condition_variable loopGuard; + std::mutex loopMutex; + + std::mutex requestMutex; + + std::vector requests; + bool pending = false; + + bool busy = false; + } asyncLoopState; + struct SPreloadTarget { eTargetType type = TARGET_IMAGE; std::string id = ""; diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp index 37eb6ac..0a6728e 100644 --- a/src/renderer/Renderer.cpp +++ b/src/renderer/Renderer.cpp @@ -14,6 +14,7 @@ #include "widgets/PasswordInputField.hpp" #include "widgets/Background.hpp" +#include "widgets/Label.hpp" inline const float fullVerts[] = { 1, 0, // top right @@ -131,6 +132,10 @@ CRenderer::SRenderFeedback CRenderer::renderLock(const CSessionLockSurface& surf SRenderFeedback feedback; + const float bga = asyncResourceGatherer->applied ? + std::clamp(std::chrono::duration_cast(std::chrono::system_clock::now() - gatheredAt).count() / 500000.0, 0.0, 1.0) : + 0.0; + if (!asyncResourceGatherer->ready) { // render status if (!**PDISABLEBAR) { @@ -147,7 +152,7 @@ CRenderer::SRenderFeedback CRenderer::renderLock(const CSessionLockSurface& surf // render widgets const auto WIDGETS = getOrCreateWidgetsFor(&surf); for (auto& w : *WIDGETS) { - feedback.needsFrame = w->draw() || feedback.needsFrame; + feedback.needsFrame = w->draw({bga}) || feedback.needsFrame; } } @@ -155,7 +160,7 @@ CRenderer::SRenderFeedback CRenderer::renderLock(const CSessionLockSurface& surf Debug::log(TRACE, "frame {}", frames); - feedback.needsFrame = feedback.needsFrame || !asyncResourceGatherer->ready; + feedback.needsFrame = feedback.needsFrame || !asyncResourceGatherer->ready || bga < 1.0; return feedback; } @@ -248,11 +253,13 @@ std::vector>* CRenderer::getOrCreateWidgetsFor(const CS // by type if (c.type == "background") widgets[surf].emplace_back(std::make_unique(surf->size, std::string{"background:"} + std::any_cast(c.values.at("path")))); - if (c.type == "input-field") { + else if (c.type == "input-field") { const auto SIZE = std::any_cast(c.values.at("size")); widgets[surf].emplace_back(std::make_unique( surf->size, Vector2D{SIZE.x, SIZE.y}, std::any_cast(c.values.at("outer_color")), std::any_cast(c.values.at("inner_color")), std::any_cast(c.values.at("outline_thickness")), std::any_cast(c.values.at("fade_on_empty")))); + } else if (c.type == "label") { + widgets[surf].emplace_back(std::make_unique(surf->size, c.values)); } } } diff --git a/src/renderer/widgets/Background.cpp b/src/renderer/widgets/Background.cpp index 413e728..86f43f1 100644 --- a/src/renderer/widgets/Background.cpp +++ b/src/renderer/widgets/Background.cpp @@ -5,17 +5,16 @@ CBackground::CBackground(const Vector2D& viewport_, const std::string& resourceI ; } -bool CBackground::draw() { +bool CBackground::draw(const SRenderData& data) { if (!asset) asset = g_pRenderer->asyncResourceGatherer->getAssetByID(resourceID); if (!asset) return false; - float bga = std::clamp(std::chrono::duration_cast(std::chrono::system_clock::now() - g_pRenderer->gatheredAt).count() / 500000.0, 0.0, 1.0); - CBox monbox = {0, 0, viewport.x, viewport.y}; + CBox monbox = {0, 0, viewport.x, viewport.y}; - g_pRenderer->renderTexture(monbox, asset->texture, bga); + g_pRenderer->renderTexture(monbox, asset->texture, data.opacity); - return bga < 1.0; + return data.opacity < 1.0; } \ No newline at end of file diff --git a/src/renderer/widgets/Background.hpp b/src/renderer/widgets/Background.hpp index a99aa1d..9845c41 100644 --- a/src/renderer/widgets/Background.hpp +++ b/src/renderer/widgets/Background.hpp @@ -10,7 +10,7 @@ class CBackground : public IWidget { public: CBackground(const Vector2D& viewport, const std::string& resourceID); - virtual bool draw(); + virtual bool draw(const SRenderData& data); private: Vector2D viewport; diff --git a/src/renderer/widgets/IWidget.hpp b/src/renderer/widgets/IWidget.hpp index 1f667dd..45a6e63 100644 --- a/src/renderer/widgets/IWidget.hpp +++ b/src/renderer/widgets/IWidget.hpp @@ -2,5 +2,9 @@ class IWidget { public: - virtual bool draw() = 0; + struct SRenderData { + float opacity = 1; + }; + + virtual bool draw(const SRenderData& data) = 0; }; \ No newline at end of file diff --git a/src/renderer/widgets/Label.cpp b/src/renderer/widgets/Label.cpp new file mode 100644 index 0000000..8daf1c4 --- /dev/null +++ b/src/renderer/widgets/Label.cpp @@ -0,0 +1,81 @@ +#include "Label.hpp" +#include "../../helpers/Color.hpp" +#include +#include "../Renderer.hpp" +#include "../../helpers/Log.hpp" + +void replaceAll(std::string& str, const std::string& from, const std::string& to) { + if (from.empty()) + return; + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } +} + +std::string CLabel::formatString(std::string in) { + replaceAll(in, "$USER", std::string{getlogin()}); + return in; +} + +CLabel::CLabel(const Vector2D& viewport_, const std::unordered_map& props) { + std::string labelPreFormat = std::any_cast(props.at("text")); + std::string fontFamily = std::any_cast(props.at("font_family")); + CColor labelColor = std::any_cast(props.at("color")); + int fontSize = std::any_cast(props.at("font_size")); + + CAsyncResourceGatherer::SPreloadRequest request; + request.id = std::string{"label:"} + std::to_string((uintptr_t)this); + resourceID = request.id; + request.asset = formatString(labelPreFormat); + request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT; + request.props["font_family"] = fontFamily; + request.props["color"] = labelColor; + request.props["font_size"] = fontSize; + + g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request); + + auto POS__ = std::any_cast(props.at("position")); + pos = {POS__.x, POS__.y}; + + viewport = viewport_; + label = request.asset; + + halign = std::any_cast(props.at("halign")); + valign = std::any_cast(props.at("valign")); +} + +bool CLabel::draw(const SRenderData& data) { + if (!asset) { + asset = g_pRenderer->asyncResourceGatherer->getAssetByID(resourceID); + + if (!asset) + return true; + + // calc pos + if (halign == "center") + pos.x += viewport.x / 2.0 - asset->texture.m_vSize.x / 2.0; + else if (halign == "left") + pos.x += 0; + else if (halign == "right") + pos.x += viewport.x - asset->texture.m_vSize.x; + else if (halign != "none") + Debug::log(ERR, "Label: invalid halign {}", halign); + + if (valign == "center") + pos.y += viewport.y / 2.0 - asset->texture.m_vSize.y / 2.0; + else if (valign == "top") + pos.y += viewport.y - asset->texture.m_vSize.y; + else if (valign == "bottom") + pos.y += asset->texture.m_vSize.y; + else if (valign != "none") + Debug::log(ERR, "Label: invalid halign {}", halign); + } + + CBox box = {pos.x, pos.y, asset->texture.m_vSize.x, asset->texture.m_vSize.y}; + + g_pRenderer->renderTexture(box, asset->texture, data.opacity); + + return false; +} \ No newline at end of file diff --git a/src/renderer/widgets/Label.hpp b/src/renderer/widgets/Label.hpp new file mode 100644 index 0000000..35a35c6 --- /dev/null +++ b/src/renderer/widgets/Label.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "IWidget.hpp" +#include "../../helpers/Vector2D.hpp" +#include +#include +#include + +struct SPreloadedAsset; + +class CLabel : public IWidget { + public: + CLabel(const Vector2D& viewport, const std::unordered_map& props); + + virtual bool draw(const SRenderData& data); + + private: + std::string formatString(std::string in); + + Vector2D viewport; + Vector2D pos; + std::string resourceID; + std::string label; + std::string halign, valign; + SPreloadedAsset* asset = nullptr; +}; \ No newline at end of file diff --git a/src/renderer/widgets/PasswordInputField.cpp b/src/renderer/widgets/PasswordInputField.cpp index 4ca7718..06ab43b 100644 --- a/src/renderer/widgets/PasswordInputField.cpp +++ b/src/renderer/widgets/PasswordInputField.cpp @@ -72,7 +72,7 @@ void CPasswordInputField::updateDots() { std::erase_if(dots, [](const auto& dot) { return !dot.appearing && dot.a == 0.0; }); } -bool CPasswordInputField::draw() { +bool CPasswordInputField::draw(const SRenderData& data) { CBox inputFieldBox = {pos, size}; CBox outerBox = {pos - Vector2D{out_thick, out_thick}, size + Vector2D{out_thick * 2, out_thick * 2}}; @@ -80,9 +80,9 @@ bool CPasswordInputField::draw() { updateDots(); CColor outerCol = outer; - outer.a = fade.a; + outer.a = fade.a * data.opacity; CColor innerCol = inner; - innerCol.a = fade.a; + innerCol.a = fade.a * data.opacity; g_pRenderer->renderRect(outerBox, outerCol, outerBox.h / 2.0); g_pRenderer->renderRect(inputFieldBox, innerCol, inputFieldBox.h / 2.0); @@ -93,7 +93,7 @@ bool CPasswordInputField::draw() { for (size_t i = 0; i < dots.size(); ++i) { Vector2D currentPos = inputFieldBox.pos() + Vector2D{PASS_SPACING, inputFieldBox.h / 2.f - PASS_SIZE / 2.f} + Vector2D{(PASS_SIZE + PASS_SPACING) * dots[i].idx, 0}; CBox box{currentPos, Vector2D{PASS_SIZE, PASS_SIZE}}; - g_pRenderer->renderRect(box, CColor{0, 0, 0, dots[i].a}, PASS_SIZE / 2.0); + g_pRenderer->renderRect(box, CColor{0, 0, 0, dots[i].a * data.opacity}, PASS_SIZE / 2.0); } return std::ranges::any_of(dots.begin(), dots.end(), [](const auto& dot) { return dot.animated; }) || fade.animated; diff --git a/src/renderer/widgets/PasswordInputField.hpp b/src/renderer/widgets/PasswordInputField.hpp index 8c8d71a..5f73f64 100644 --- a/src/renderer/widgets/PasswordInputField.hpp +++ b/src/renderer/widgets/PasswordInputField.hpp @@ -10,7 +10,7 @@ class CPasswordInputField : public IWidget { public: CPasswordInputField(const Vector2D& viewport, const Vector2D& size, const CColor& outer, const CColor& inner, int out_thick, bool fade_empty); - virtual bool draw(); + virtual bool draw(const SRenderData& data); private: void updateDots();