Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This mode tries recreating the stretching and squishing that is done to moving o

https://github.com/VirtCode/hypr-dynamic-cursors/assets/41426325/7b8289e7-9dd2-4b57-b406-4fa28779a260

### edge squash
The cursor squashes when it gets close to screen edges, like it's being pressed against them. It flattens perpendicular to the edge and stretches along it. In corners, it squashes diagonally. There's also some spring physics so it bounces back smoothly when you pull away. The effect works independently of the simulation modes above and is enabled by default.

### shake to find
The plugin supports shake to find, akin to how KDE Plasma, MacOS, etc. do it. It can also be extensively configured and is enabled by default. It also supports using [hyprcursor](https://github.com/hyprwm/hyprcursor) for high resolution cursor images. The magnification can also be triggered as a dispatcher instead of on shake. If you only want shake to find, and no weird cursor behaviour, you can disable the above modes with the mode `none`.

Expand Down Expand Up @@ -170,6 +173,31 @@ plugin:dynamic-cursors {
window = 100
}

# configure edge squash effect
# squashes the cursor when near screen edges
edge_squash {

# enables edge squash
enabled = true

# distance from edge in pixels to start the effect
distance = 130

# strength of the squash effect (1.0 = full squash at edge)
strength = 1.0

# distance from corner (in px) to trigger diagonal squashing
# only applies when close to both edges simultaneously
corner_radius = 60

# spring physics for bounce-back effect
# how quickly cursor bounces back (0.0 - 1.0, higher = faster)
spring_stiffness = 0.15

# prevents oscillation (0.0 - 1.0, higher = more damping)
spring_damping = 0.7
}

# configure shake to find
# magnifies the cursor if its is being shaken
shake {
Expand Down
54 changes: 27 additions & 27 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/config/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
#define CONFIG_SHAKE_LIMIT "shake:limit"
#define CONFIG_SHAKE_TIMEOUT "shake:timeout"

#define CONFIG_EDGE_ENABLED "edge_squash:enabled"
#define CONFIG_EDGE_DISTANCE "edge_squash:distance"
#define CONFIG_EDGE_STRENGTH "edge_squash:strength"
#define CONFIG_EDGE_CORNER_RADIUS "edge_squash:corner_radius"
#define CONFIG_EDGE_SPRING_STIFFNESS "edge_squash:spring_stiffness"
#define CONFIG_EDGE_SPRING_DAMPING "edge_squash:spring_damping"

#define CONFIG_ROTATE_LENGTH "rotate:length"
#define CONFIG_ROTATE_OFFSET "rotate:offset"

Expand Down
17 changes: 17 additions & 0 deletions src/cursor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ void CDynamicCursors::onCursorMoved(CPointerManager* pointers) {
if (mode) mode->warp(lastPos, pointers->m_pointerPos);

if (**PSHAKE) shake.warp(lastPos, pointers->m_pointerPos);

edgeSquash.warp(lastPos, pointers->m_pointerPos);
}

calculate(MOVE);
Expand Down Expand Up @@ -436,9 +438,24 @@ void CDynamicCursors::calculate(EModeUpdate type) {
if (resultShake > 1 && !**PSHAKE_EFFECTS) resultMode = SModeResult();
} else resultShake = 1;

// calculate edge squash
auto edgeResult = edgeSquash.update(g_pPointerManager->m_pointerPos);

auto result = resultMode;
result.scale *= resultShake;

// combine edge squash with mode result
if (edgeResult.stretch.magnitude.x != 1.0 || edgeResult.stretch.magnitude.y != 1.0) {
// If mode has no stretch, use edge squash directly
if (result.stretch.magnitude.x == 1.0 && result.stretch.magnitude.y == 1.0) {
result.stretch = edgeResult.stretch;
} else {
// Combine stretches by multiplying magnitudes
result.stretch.magnitude.x *= edgeResult.stretch.magnitude.x;
result.stretch.magnitude.y *= edgeResult.stretch.magnitude.y;
}
}

if (resultShown.hasDifference(&result, **PTHRESHOLD * (PI / 180.0), 0.01, 0.01)) {
resultShown = result;
resultShown.clamp(**PTHRESHOLD * (PI / 180.0), 0.01, 0.01); // clamp low values so it is rendered pixel-perfectly when no effect
Expand Down
4 changes: 4 additions & 0 deletions src/cursor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "mode/ModeTilt.hpp"
#include "mode/ModeStretch.hpp"
#include "other/Shake.hpp"
#include "other/EdgeSquash.hpp"
#include "highres.hpp"

class CDynamicCursors {
Expand Down Expand Up @@ -73,6 +74,9 @@ class CDynamicCursors {
// shake
CShake shake;

// edge squash
CEdgeSquash edgeSquash;

/* is set true if a genuine move is being performed, and will be reset to false after onCursorMoved */
bool isMove = false;

Expand Down
7 changes: 7 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
addConfig(CONFIG_SHAKE_LIMIT, 0.0F);
addConfig(CONFIG_SHAKE_TIMEOUT, 2000);

addConfig(CONFIG_EDGE_ENABLED, true);
addConfig(CONFIG_EDGE_DISTANCE, 130);
addConfig(CONFIG_EDGE_STRENGTH, 1.0f);
addConfig(CONFIG_EDGE_CORNER_RADIUS, 60);
addConfig(CONFIG_EDGE_SPRING_STIFFNESS, 0.15f);
addConfig(CONFIG_EDGE_SPRING_DAMPING, 0.7f);

addConfig(CONFIG_HIGHRES_ENABLED, true);
addConfig(CONFIG_HIGHRES_NEAREST, true);
addConfig(CONFIG_HIGHRES_FALLBACK, "clientside");
Expand Down
141 changes: 141 additions & 0 deletions src/other/EdgeSquash.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include "../globals.hpp"
#include "../config/config.hpp"
#include "EdgeSquash.hpp"

#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/helpers/Monitor.hpp>
#include <cmath>

SModeResult CEdgeSquash::update(Vector2D pos) {
static auto* const* PENABLED = (Hyprlang::INT* const*) getConfig(CONFIG_EDGE_ENABLED);
static auto* const* PDISTANCE = (Hyprlang::INT* const*) getConfig(CONFIG_EDGE_DISTANCE);
static auto* const* PSTRENGTH = (Hyprlang::FLOAT* const*) getConfig(CONFIG_EDGE_STRENGTH);
static auto* const* PCORNER_RADIUS = (Hyprlang::INT* const*) getConfig(CONFIG_EDGE_CORNER_RADIUS);
static auto* const* PSPRING_STIFFNESS = (Hyprlang::FLOAT* const*) getConfig(CONFIG_EDGE_SPRING_STIFFNESS);
static auto* const* PSPRING_DAMPING = (Hyprlang::FLOAT* const*) getConfig(CONFIG_EDGE_SPRING_DAMPING);

auto result = SModeResult();

if (!**PENABLED)
return result;

// Find the monitor containing the cursor
SP<CMonitor> pMonitor = nullptr;
for (auto& m : g_pCompositor->m_monitors) {
if (!m->m_output)
continue;

auto box = m->logicalBox();
if (box.containsPoint(pos)) {
pMonitor = m;
break;
}
}

if (!pMonitor)
return result;

auto box = pMonitor->logicalBox();
int distance = **PDISTANCE;
float strength = **PSTRENGTH;

// Calculate distance to each edge in pixels
double distLeft = pos.x - box.x;
double distRight = (box.x + box.w) - pos.x;
double distTop = pos.y - box.y;
double distBottom = (box.y + box.h) - pos.y;

// Find closest edges - we need this to determine which edge(s) are affecting the cursor
double minHorizontal = std::min(distLeft, distRight);
double minVertical = std::min(distTop, distBottom);
double minDist = std::min(minHorizontal, minVertical);

// Calculate raw squash factor using cubic easing
// This determines how much squashing we want before applying spring physics
double rawSquashFactor = 0.0;
if (minDist <= distance) {
// t ranges from 0.0 (at trigger distance) to 1.0 (at edge)
double t = 1.0 - (minDist / distance);
// Cubic easing (t³) gives smooth acceleration as cursor approaches edge
// Linear would feel mechanical, quadratic too sudden, cubic feels natural
rawSquashFactor = t * t * t * strength;
}

// Spring physics for bounce-back effect
// This simulates a spring connecting the current squash to the target squash
double springStiffness = **PSPRING_STIFFNESS;
double springDamping = **PSPRING_DAMPING;

// Hooke's law: F = k * x
// Force is proportional to displacement from target (rawSquashFactor - lastSquashFactor)
// Higher stiffness = faster response to changes
double springForce = (rawSquashFactor - lastSquashFactor) * springStiffness;
squashVelocity += springForce;
// Damping reduces velocity each frame to prevent oscillation
// Without damping, the cursor would bounce back and forth endlessly
squashVelocity *= springDamping;

// Semi-implicit Euler integration: update position using new velocity
double squashFactor = lastSquashFactor + squashVelocity;
squashFactor = std::clamp(squashFactor, 0.0, 1.0);

lastSquashFactor = squashFactor;

if (squashFactor < 0.01)
return result;

// Corner detection: determine if we should blend between edge squash and diagonal squash
double cornerThreshold = **PCORNER_RADIUS;

// cornerInfluence ranges from 0.0 (pure edge) to 1.0 (pure corner diagonal)
// This controls the blend between cardinal angles (0°/90°) and natural diagonal angles
double cornerInfluence = 0.0;
if (minHorizontal <= cornerThreshold && minVertical <= cornerThreshold) {
// Both edges are close enough - calculate corner influence
// Ratios range from 0.0 (at threshold distance) to 1.0 (at corner)
double horizontalRatio = 1.0 - (minHorizontal / cornerThreshold);
double verticalRatio = 1.0 - (minVertical / cornerThreshold);
// Use minimum so both edges must be close for full corner effect
// This prevents corners from activating when only one edge is near
cornerInfluence = std::min(horizontalRatio, verticalRatio);
// Apply cubic function (x³) for ultra-smooth transition
// This makes the transition feel more organic and less sudden
cornerInfluence = cornerInfluence * cornerInfluence * cornerInfluence;
}

// Calculate natural diagonal angle based on relative position between edges
// angleRatio represents the balance between horizontal and vertical edge influence
// Example: if minHorizontal=30, minVertical=90, then angleRatio=30/(30+90)=0.25 (mostly horizontal)
double angleRatio = minHorizontal / (minHorizontal + minVertical + 0.001); // add epsilon to avoid division by zero
// Map angleRatio (0.0 to 1.0) to angle (0° to 90°)
// angleRatio=0.0 means horizontal edge is closest → 0° (horizontal squash)
// angleRatio=1.0 means vertical edge is closest → 90° (vertical squash)
double naturalAngle = angleRatio * PI / 2;

// Determine which cardinal direction (0° or 90°) we should use when not in a corner
// This provides the "snap to edge" behavior when far from corners
double cardinalAngle;
if (minHorizontal < minVertical) {
cardinalAngle = PI / 2; // closer to vertical edge → 90° (vertical squash)
} else {
cardinalAngle = 0; // closer to horizontal edge → 0° (horizontal squash)
}

// Linear interpolation (lerp) between cardinal and natural angle
// When cornerInfluence = 0: use cardinalAngle (snap to 0° or 90°)
// When cornerInfluence = 1: use naturalAngle (smooth diagonal from 0° to 90°)
// Formula: result = A * (1 - t) + B * t, where t is the blend factor
result.stretch.angle = cardinalAngle * (1.0 - cornerInfluence) + naturalAngle * cornerInfluence;

// Apply squash/stretch to cursor
// x: stretch along the edge (3.5x at full squash)
// y: compress perpendicular to edge (5% of original at full squash)
result.stretch.magnitude = Vector2D{1.0 + squashFactor * 3.5, 1.0 - squashFactor * 0.95};

lastPos = pos;
return result;
}

void CEdgeSquash::warp(Vector2D old, Vector2D pos) {
lastPos = pos;
}
Loading