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 {