From 149b6737c263b892ff7f591baeae733b98f13fac Mon Sep 17 00:00:00 2001 From: bvr-yr <130279855+bvr-yr@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:44:58 +0300 Subject: [PATCH] input-field: fail display improvments (#154) * input-field: fail display improvments * update Home Manager * add `$ATTEMPTS` variable, change defaults * nix wording * log failed attempts --- nix/hm-module.nix | 21 +++ src/config/ConfigManager.cpp | 6 + src/core/hyprlock.cpp | 6 + src/core/hyprlock.hpp | 4 +- src/renderer/widgets/PasswordInputField.cpp | 156 +++++++++++++++++--- src/renderer/widgets/PasswordInputField.hpp | 29 ++-- 6 files changed, 187 insertions(+), 35 deletions(-) diff --git a/nix/hm-module.nix b/nix/hm-module.nix index b8faa87..5ea017f 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -261,6 +261,24 @@ in { default = -1; }; + fail_color = mkOption { + description = "If authentication failed, changes outer color and fail message color"; + type = str; + default = "rgb(204, 34, 34)"; + }; + + fail_text = mkOption { + description = "The text shown if authentication failed. $FAIL (reason) and $ATTEMPTS variables are available"; + type = str; + default = "$FAIL"; + }; + + fail_transition = mkOption { + description = "The transition time (ms) between normal outer color and fail color"; + type = int; + default = 300; + }; + position = { x = mkOption { description = "X position of the label"; @@ -414,6 +432,9 @@ in { shadow_size = ${toString input-field.shadow_size} shadow_color = ${input-field.shadow_color} shadow_boost = ${toString input-field.shadow_boost} + fail_color = ${input-field.fail_color} + fail_text = ${input-field.fail_text} + fail_transition = ${toString input-field.fail_transition} position = ${toString input-field.position.x}, ${toString input-field.position.y} halign = ${input-field.halign} diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 97e8dba..679c0f5 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -78,6 +78,9 @@ void CConfigManager::init() { m_config.addSpecialConfigValue("input-field", "placeholder_text", Hyprlang::STRING{"Input Password"}); m_config.addSpecialConfigValue("input-field", "hide_input", Hyprlang::INT{0}); m_config.addSpecialConfigValue("input-field", "rounding", Hyprlang::INT{-1}); + m_config.addSpecialConfigValue("input-field", "fail_color", Hyprlang::INT{0xFFCC2222}); + m_config.addSpecialConfigValue("input-field", "fail_text", Hyprlang::STRING{"$FAIL"}); + m_config.addSpecialConfigValue("input-field", "fail_transition", Hyprlang::INT{300}); SHADOWABLE("input-field"); m_config.addSpecialCategory("label", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); @@ -165,6 +168,9 @@ std::vector CConfigManager::getWidgetConfigs() { {"placeholder_text", m_config.getSpecialConfigValue("input-field", "placeholder_text", k.c_str())}, {"hide_input", m_config.getSpecialConfigValue("input-field", "hide_input", k.c_str())}, {"rounding", m_config.getSpecialConfigValue("input-field", "rounding", k.c_str())}, + {"fail_color", m_config.getSpecialConfigValue("input-field", "fail_color", k.c_str())}, + {"fail_text", m_config.getSpecialConfigValue("input-field", "fail_text", k.c_str())}, + {"fail_transition", m_config.getSpecialConfigValue("input-field", "fail_transition", k.c_str())}, SHADOWABLE("input-field"), } }); diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index ee961c3..383ffc3 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -681,6 +681,8 @@ void CHyprlock::onPasswordCheckTimer() { Debug::log(LOG, "Authentication failed: {}", m_sPasswordState.result->failReason); m_sPasswordState.lastFailReason = m_sPasswordState.result->failReason; m_sPasswordState.passBuffer = ""; + m_sPasswordState.failedAttempts += 1; + Debug::log(LOG, "Failed attempts: {}", m_sPasswordState.failedAttempts); for (auto& o : m_vOutputs) { o->sessionLockSurface->render(); @@ -820,6 +822,10 @@ size_t CHyprlock::getPasswordBufferLen() { return m_sPasswordState.passBuffer.length(); } +size_t CHyprlock::getPasswordFailedAttempts() { + return m_sPasswordState.failedAttempts; +} + std::shared_ptr CHyprlock::addTimer(const std::chrono::system_clock::duration& timeout, std::function self, void* data)> cb_, void* data) { std::lock_guard lg(m_sLoopState.timersMutex); const auto T = m_vTimers.emplace_back(std::make_shared(timeout, cb_, data)); diff --git a/src/core/hyprlock.hpp b/src/core/hyprlock.hpp index 77a5cd8..06d9e08 100644 --- a/src/core/hyprlock.hpp +++ b/src/core/hyprlock.hpp @@ -54,6 +54,7 @@ class CHyprlock { std::optional passwordLastFailReason(); size_t getPasswordBufferLen(); + size_t getPasswordFailedAttempts(); ext_session_lock_manager_v1* getSessionLockMgr(); ext_session_lock_v1* getSessionLock(); @@ -117,6 +118,7 @@ class CHyprlock { std::string passBuffer = ""; std::shared_ptr result; std::optional lastFailReason; + size_t failedAttempts = 0; } m_sPasswordState; struct { @@ -136,4 +138,4 @@ class CHyprlock { std::vector m_vPressedKeys; }; -inline std::unique_ptr g_pHyprlock; \ No newline at end of file +inline std::unique_ptr g_pHyprlock; diff --git a/src/renderer/widgets/PasswordInputField.cpp b/src/renderer/widgets/PasswordInputField.cpp index 48a9bde..0ccffc0 100644 --- a/src/renderer/widgets/PasswordInputField.cpp +++ b/src/renderer/widgets/PasswordInputField.cpp @@ -4,25 +4,35 @@ #include CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::unordered_map& props) : shadow(this, props, viewport_) { - size = std::any_cast(props.at("size")); - inner = std::any_cast(props.at("inner_color")); - outer = std::any_cast(props.at("outer_color")); - outThick = std::any_cast(props.at("outline_thickness")); - dots.size = std::any_cast(props.at("dots_size")); - dots.spacing = std::any_cast(props.at("dots_spacing")); - dots.center = std::any_cast(props.at("dots_center")); - dots.rounding = std::any_cast(props.at("dots_rounding")); - fadeOnEmpty = std::any_cast(props.at("fade_on_empty")); - fadeTimeoutMs = std::any_cast(props.at("fade_timeout")); - font = std::any_cast(props.at("font_color")); - pos = std::any_cast(props.at("position")); - hiddenInputState.enabled = std::any_cast(props.at("hide_input")); - rounding = std::any_cast(props.at("rounding")); - viewport = viewport_; + size = std::any_cast(props.at("size")); + inner = std::any_cast(props.at("inner_color")); + outer = std::any_cast(props.at("outer_color")); + outThick = std::any_cast(props.at("outline_thickness")); + dots.size = std::any_cast(props.at("dots_size")); + dots.spacing = std::any_cast(props.at("dots_spacing")); + dots.center = std::any_cast(props.at("dots_center")); + dots.rounding = std::any_cast(props.at("dots_rounding")); + fadeOnEmpty = std::any_cast(props.at("fade_on_empty")); + fadeTimeoutMs = std::any_cast(props.at("fade_timeout")); + font = std::any_cast(props.at("font_color")); + hiddenInputState.enabled = std::any_cast(props.at("hide_input")); + rounding = std::any_cast(props.at("rounding")); + placeholder.failColor = std::any_cast(props.at("fail_color")); + placeholder.failTransitionMs = std::any_cast(props.at("fail_transition")); + configFailText = std::any_cast(props.at("fail_text")); + viewport = viewport_; - pos = posFromHVAlign(viewport, size, pos, std::any_cast(props.at("halign")), std::any_cast(props.at("valign"))); - dots.size = std::clamp(dots.size, 0.2f, 0.8f); - dots.spacing = std::clamp(dots.spacing, 0.f, 1.f); + auto POS__ = std::any_cast(props.at("position")); + pos = {POS__.x, POS__.y}; + configPos = pos; + + halign = std::any_cast(props.at("halign")); + valign = std::any_cast(props.at("valign")); + + pos = posFromHVAlign(viewport, size, pos, halign, valign); + dots.size = std::clamp(dots.size, 0.2f, 0.8f); + dots.spacing = std::clamp(dots.spacing, 0.f, 1.f); + placeholder.failTransitionMs = std::clamp(placeholder.failTransitionMs, 1, 5000); std::string placeholderText = std::any_cast(props.at("placeholder_text")); if (!placeholderText.empty()) { @@ -38,6 +48,16 @@ CPasswordInputField::CPasswordInputField(const Vector2D& viewport_, const std::u } } +static void replaceAllFail(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(); + } +} + static void fadeOutCallback(std::shared_ptr self, void* data) { CPasswordInputField* p = (CPasswordInputField*)data; @@ -93,8 +113,10 @@ void CPasswordInputField::updateFade() { else fade.a = std::clamp(1.0 - std::chrono::duration_cast(std::chrono::system_clock::now() - fade.start).count() / 100000.0, 0.0, 1.0); - if ((fade.appearing && fade.a == 1.0) || (!fade.appearing && fade.a == 0.0)) + if ((fade.appearing && fade.a == 1.0) || (!fade.appearing && fade.a == 0.0)) { fade.animated = false; + redrawShadow = true; + } } } @@ -130,8 +152,9 @@ bool CPasswordInputField::draw(const SRenderData& data) { CBox inputFieldBox = {pos, size}; CBox outerBox = {pos - Vector2D{outThick, outThick}, size + Vector2D{outThick * 2, outThick * 2}}; - if (firstRender) { - firstRender = false; + if (firstRender || redrawShadow) { + firstRender = false; + redrawShadow = false; shadow.markShadowDirty(); } @@ -140,8 +163,23 @@ bool CPasswordInputField::draw(const SRenderData& data) { updateFade(); updateDots(); updateFailTex(); + updateOuter(); updateHiddenInputState(); + static auto ORIGSIZEX = size.x; + static auto ORIGPOS = pos; + + if (placeholder.failAsset && placeholder.failAsset->texture.m_vSize.x > ORIGSIZEX) { + if (placeholder.failAsset->texture.m_vSize.x > size.x) + redrawShadow = true; + + size.x = placeholder.failAsset->texture.m_vSize.x + inputFieldBox.h; + pos = posFromHVAlign(viewport, size, configPos, halign, valign); + } else { + size.x = ORIGSIZEX; + pos = ORIGPOS; + } + SRenderData shadowData = data; shadowData.opacity *= fade.a; shadow.draw(shadowData); @@ -241,7 +279,7 @@ bool CPasswordInputField::draw(const SRenderData& data) { forceReload = true; } - return dots.currentAmount != PASSLEN || fade.animated || data.opacity < 1.0 || forceReload; + return dots.currentAmount != PASSLEN || fade.animated || outerAnimated || redrawShadow || data.opacity < 1.0 || forceReload; } void CPasswordInputField::updateFailTex() { @@ -255,6 +293,7 @@ void CPasswordInputField::updateFailTex() { g_pRenderer->asyncResourceGatherer->unloadAsset(placeholder.failAsset); placeholder.failAsset = nullptr; placeholder.failID = ""; + redrawShadow = true; } return; } @@ -262,14 +301,18 @@ void CPasswordInputField::updateFailTex() { if (!FAIL.has_value() || !placeholder.canGetNewFail) return; + placeholder.failText = configFailText; + replaceAllFail(placeholder.failText, "$FAIL", FAIL.value()); + replaceAllFail(placeholder.failText, "$ATTEMPTS", std::to_string(g_pHyprlock->getPasswordFailedAttempts())); + // query CAsyncResourceGatherer::SPreloadRequest request; request.id = "input-error:" + std::to_string((uintptr_t)this) + ",time:" + std::to_string(time(nullptr)); placeholder.failID = request.id; - request.asset = "" + FAIL.value() + ""; + request.asset = placeholder.failText; request.type = CAsyncResourceGatherer::eTargetType::TARGET_TEXT; request.props["font_family"] = std::string{"Sans"}; - request.props["color"] = CColor{1.0 - font.r, 1.0 - font.g, 1.0 - font.b, 0.5}; + request.props["color"] = placeholder.failColor; request.props["font_size"] = (int)size.y / 4; g_pRenderer->asyncResourceGatherer->requestAsyncAssetPreload(request); @@ -302,3 +345,68 @@ void CPasswordInputField::updateHiddenInputState() { hiddenInputState.lastColor.a = 1.0; hiddenInputState.lastQuadrant = (hiddenInputState.lastQuadrant + rand() % 3 + 1) % 4; } + +void CPasswordInputField::updateOuter() { + if (outThick == 0) + return; + + static auto OUTERCOL = outer; + static auto TIMER = std::chrono::system_clock::now(); + bool changeToOuter = placeholder.failID.empty(); + + outerAnimated = false; + + if (changeToOuter) { + if (outer == OUTERCOL) + return; + + if (outer == placeholder.failColor) + TIMER = std::chrono::system_clock::now(); + } else if (!changeToOuter) { + if (fade.animated || fade.a < 1.0) + changeToOuter = true; + + if (outer == OUTERCOL) + TIMER = std::chrono::system_clock::now(); + } + + const auto MULTI = std::clamp( + std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - TIMER).count() / (double)placeholder.failTransitionMs, 0.001, 0.5); + const auto DELTA = changeToOuter ? OUTERCOL - placeholder.failColor : placeholder.failColor - OUTERCOL; + const auto TARGET = changeToOuter ? OUTERCOL : placeholder.failColor; + const auto SOURCE = changeToOuter ? placeholder.failColor : OUTERCOL; + + if (outer.r != TARGET.r) { + outer.r += DELTA.r * MULTI; + outerAnimated = true; + + if ((SOURCE.r < TARGET.r && outer.r > TARGET.r) || (SOURCE.r > TARGET.r && outer.r < TARGET.r)) + outer.r = TARGET.r; + } + + if (outer.g != TARGET.g) { + outer.g += DELTA.g * MULTI; + outerAnimated = true; + + if ((SOURCE.g < TARGET.g && outer.g > TARGET.g) || (SOURCE.g > TARGET.g && outer.g < TARGET.g)) + outer.g = TARGET.g; + } + + if (outer.b != TARGET.b) { + outer.b += DELTA.b * MULTI; + outerAnimated = true; + + if ((SOURCE.b < TARGET.b && outer.b > TARGET.b) || (SOURCE.b > TARGET.b && outer.b < TARGET.b)) + outer.b = TARGET.b; + } + + if (outer.a != TARGET.a) { + outer.a += DELTA.a * MULTI; + outerAnimated = true; + + if ((SOURCE.a < TARGET.a && outer.a > TARGET.a) || (SOURCE.a > TARGET.a && outer.a < TARGET.a)) + outer.a = TARGET.a; + } + + TIMER = std::chrono::system_clock::now(); +} diff --git a/src/renderer/widgets/PasswordInputField.hpp b/src/renderer/widgets/PasswordInputField.hpp index f81ed63..b6d16f1 100644 --- a/src/renderer/widgets/PasswordInputField.hpp +++ b/src/renderer/widgets/PasswordInputField.hpp @@ -20,20 +20,26 @@ class CPasswordInputField : public IWidget { void onFadeOutTimer(); private: - void updateDots(); - void updateFade(); - void updateFailTex(); - void updateHiddenInputState(); + void updateDots(); + void updateFade(); + void updateFailTex(); + void updateHiddenInputState(); + void updateOuter(); - bool firstRender = true; + bool firstRender = true; + bool redrawShadow = false; + bool outerAnimated = false; - Vector2D size; - Vector2D pos; - Vector2D viewport; + Vector2D size; + Vector2D pos; + Vector2D viewport; + Vector2D configPos; - int outThick, rounding; + std::string halign, valign, configFailText; - CColor inner, outer, font; + int outThick, rounding; + + CColor inner, outer, font; struct { float currentAmount = 0; @@ -61,6 +67,9 @@ class CPasswordInputField : public IWidget { std::string failID = ""; SPreloadedAsset* failAsset = nullptr; bool canGetNewFail = true; + CColor failColor; + int failTransitionMs = 0; + std::string failText = ""; } placeholder; struct {