summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-02-28 18:53:26 +0000
committerGitHub <noreply@github.com>2026-02-28 18:53:26 +0000
commitb90c61c04f02ae5bbf0d1a1a63da18f3cee6919d (patch)
treeedacbe4cb2673aa1a0c4abde486353ab9bf68809
parente333a330c0ddb07db39028fa8b3a47b3f7d21b5f (diff)
compositor: fix focus edge detection (#13425)
fixes edge detection, making it more relaxed and intuitive
-rw-r--r--hyprtester/src/tests/main/scroll.cpp79
-rw-r--r--hyprtester/src/tests/main/window.cpp4
-rw-r--r--hyprtester/test.conf9
-rw-r--r--src/Compositor.cpp41
4 files changed, 123 insertions, 10 deletions
diff --git a/hyprtester/src/tests/main/scroll.cpp b/hyprtester/src/tests/main/scroll.cpp
new file mode 100644
index 00000000..26be6a9a
--- /dev/null
+++ b/hyprtester/src/tests/main/scroll.cpp
@@ -0,0 +1,79 @@
+#include "../shared.hpp"
+#include "../../shared.hpp"
+#include "../../hyprctlCompat.hpp"
+#include "tests.hpp"
+
+static int ret = 0;
+
+static void testFocusCycling() {
+ for (auto const& win : {"a", "b", "c", "d"}) {
+ if (!Tests::spawnKitty(win)) {
+ NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
+ ++TESTS_FAILED;
+ ret = 1;
+ return;
+ }
+ }
+
+ OK(getFromSocket("/dispatch focuswindow class:a"));
+
+ OK(getFromSocket("/dispatch movefocus r"));
+
+ {
+ auto str = getFromSocket("/activewindow");
+ EXPECT_CONTAINS(str, "class: b");
+ }
+
+ OK(getFromSocket("/dispatch movefocus r"));
+
+ {
+ auto str = getFromSocket("/activewindow");
+ EXPECT_CONTAINS(str, "class: c");
+ }
+
+ OK(getFromSocket("/dispatch movefocus r"));
+
+ {
+ auto str = getFromSocket("/activewindow");
+ EXPECT_CONTAINS(str, "class: d");
+ }
+
+ OK(getFromSocket("/dispatch movewindow l"));
+
+ {
+ auto str = getFromSocket("/activewindow");
+ EXPECT_CONTAINS(str, "class: d");
+ }
+
+ OK(getFromSocket("/dispatch movefocus u"));
+
+ {
+ auto str = getFromSocket("/activewindow");
+ EXPECT_CONTAINS(str, "class: c");
+ }
+
+ // clean up
+ NLog::log("{}Killing all windows", Colors::YELLOW);
+ Tests::killAllWindows();
+}
+
+static bool test() {
+ NLog::log("{}Testing Scroll layout", Colors::GREEN);
+
+ // setup
+ OK(getFromSocket("/dispatch workspace name:scroll"));
+ OK(getFromSocket("/keyword general:layout scrolling"));
+
+ // test
+ NLog::log("{}Testing focus cycling", Colors::GREEN);
+ testFocusCycling();
+
+ // clean up
+ NLog::log("Cleaning up", Colors::YELLOW);
+ OK(getFromSocket("/dispatch workspace 1"));
+ OK(getFromSocket("/reload"));
+
+ return !ret;
+}
+
+REGISTER_TEST_FN(test);
diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp
index 38f8ec48..c90fa863 100644
--- a/hyprtester/src/tests/main/window.cpp
+++ b/hyprtester/src/tests/main/window.cpp
@@ -93,9 +93,9 @@ static void testSwapWindow() {
{
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
- NLog::log("{}Testing kitty_A {}, swapwindow with direction 'l'", Colors::YELLOW, pos);
+ NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos);
- OK(getFromSocket("/dispatch swapwindow l"));
+ OK(getFromSocket("/dispatch swapwindow r"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
diff --git a/hyprtester/test.conf b/hyprtester/test.conf
index 56beb5ea..f249a80a 100644
--- a/hyprtester/test.conf
+++ b/hyprtester/test.conf
@@ -179,6 +179,15 @@ master {
new_status = master
}
+scrolling {
+ fullscreen_on_one_column = true
+ column_width = 0.5
+ focus_fit_method = 1
+ follow_focus = true
+ follow_min_visible = 1
+ explicit_column_widths = 0.25, 0.333, 0.5, 0.667, 0.75, 1.0
+}
+
# https://wiki.hyprland.org/Configuring/Variables/#misc
misc {
force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers
diff --git a/src/Compositor.cpp b/src/Compositor.cpp
index e027b563..2068798d 100644
--- a/src/Compositor.cpp
+++ b/src/Compositor.cpp
@@ -1404,6 +1404,35 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
PHLWINDOW leaderWindow = nullptr;
if (!useVectorAngles) {
+ // helper to check if two rectangles are adjacent along an axis, considering slight overlaps.
+ // returns true if: STICKS (delta <= 2) OR rectangles overlap but no more than 50% of the smaller dimension.
+ static auto isAdjacent = [](const double aMin, const double aMax, const double bMin, const double bMax) -> bool {
+ constexpr double STICK_THRESHOLD = 2.0;
+ constexpr double MAX_OVERLAP_RATIO = 0.5;
+
+ const double aEdge = aMin;
+ const double bEdge = bMax;
+ const double delta = aEdge - bEdge;
+
+ // old STICKS check for 2px
+ if (std::abs(delta) < STICK_THRESHOLD)
+ return true;
+
+ if (delta >= 0)
+ return false;
+
+ const double overlap = -delta;
+ const double sizeA = aMax - aMin;
+ const double sizeB = bMax - bMin;
+
+ // reject if one rectangle fully contains the other
+ if ((bMin <= aMin && bMax >= aMax) || (aMin <= bMin && aMax >= bMax))
+ return false;
+
+ // accept if overlap is at most 50% of the smaller dimension
+ return overlap <= std::min(sizeA, sizeB) * MAX_OVERLAP_RATIO;
+ };
+
for (auto const& w : m_windows) {
if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible())
continue;
@@ -1426,24 +1455,20 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
switch (dir) {
case Math::DIRECTION_LEFT:
- if (STICKS(POSA.x, POSB.x + SIZEB.x)) {
+ if (isAdjacent(POSA.x, POSA.x + SIZEA.x, POSB.x, POSB.x + SIZEB.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
- }
break;
case Math::DIRECTION_RIGHT:
- if (STICKS(POSA.x + SIZEA.x, POSB.x)) {
+ if (isAdjacent(POSB.x, POSB.x + SIZEB.x, POSA.x, POSA.x + SIZEA.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
- }
break;
case Math::DIRECTION_UP:
- if (STICKS(POSA.y, POSB.y + SIZEB.y)) {
+ if (isAdjacent(POSA.y, POSA.y + SIZEA.y, POSB.y, POSB.y + SIZEB.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
- }
break;
case Math::DIRECTION_DOWN:
- if (STICKS(POSA.y + SIZEA.y, POSB.y)) {
+ if (isAdjacent(POSB.y, POSB.y + SIZEB.y, POSA.y, POSA.y + SIZEA.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
- }
break;
default: break;
}