From b90c61c04f02ae5bbf0d1a1a63da18f3cee6919d Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:53:26 +0000 Subject: [PATCH] compositor: fix focus edge detection (#13425) fixes edge detection, making it more relaxed and intuitive --- hyprtester/src/tests/main/scroll.cpp | 79 ++++++++++++++++++++++++++++ hyprtester/src/tests/main/window.cpp | 4 +- hyprtester/test.conf | 9 ++++ src/Compositor.cpp | 41 ++++++++++++--- 4 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 hyprtester/src/tests/main/scroll.cpp 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; }