From 151b5f69783b111831e8c5dc062904a221799d7b Mon Sep 17 00:00:00 2001 From: crossatko <3613973+crossatko@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:01:05 +0200 Subject: [PATCH] dwindle: rework split logic to be fully gap-aware (#12047) --- hyprtester/src/tests/main/window.cpp | 44 +++++++--- hyprtester/src/tests/main/workspaces.cpp | 91 ++++++++++++++++++++ src/layout/DwindleLayout.cpp | 105 +++++++++++++++++------ src/layout/DwindleLayout.hpp | 13 +++ 4 files changed, 214 insertions(+), 39 deletions(-) diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp index 12e01563..6cfa061c 100644 --- a/hyprtester/src/tests/main/window.cpp +++ b/hyprtester/src/tests/main/window.cpp @@ -152,22 +152,40 @@ static bool test() { NLog::log("{}Testing window split ratios", Colors::YELLOW); { - const double RATIO = 1.25; - const double PERCENT = RATIO / 2.0 * 100.0; - const int GAPSIN = 5; - const int GAPSOUT = 20; - const int BORDERS = 2 * 2; - const int WTRIM = BORDERS + GAPSIN + GAPSOUT; - const int HEIGHT = 1080 - (BORDERS + (GAPSOUT * 2)); - const int WIDTH1 = std::round(1920.0 / 2.0 * (2 - RATIO)) - WTRIM; - const int WIDTH2 = std::round(1920.0 / 2.0 * RATIO) - WTRIM; + const double INITIAL_RATIO = 1.25; + const int GAPSIN = 5; + const int GAPSOUT = 20; + const int BORDERSIZE = 2; + const int BORDERS = BORDERSIZE * 2; + const int MONITOR_W = 1920; + const int MONITOR_H = 1080; + + const float totalAvailableHeight = MONITOR_H - (GAPSOUT * 2); + const int HEIGHT = std::round(totalAvailableHeight) - BORDERS; + const float availableWidthForSplit = MONITOR_W - (GAPSOUT * 2) - GAPSIN; + + auto calculateFinalWidth = [&](double boxWidth, bool isLeftWindow) { + double gapLeft = isLeftWindow ? GAPSOUT : GAPSIN; + double gapRight = isLeftWindow ? GAPSIN : GAPSOUT; + return std::round(boxWidth - gapLeft - gapRight - BORDERS); + }; + + double geomBoxWidthA_R1 = (availableWidthForSplit * INITIAL_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0); + double geomBoxWidthB_R1 = MONITOR_W - geomBoxWidthA_R1; + const int WIDTH1 = calculateFinalWidth(geomBoxWidthB_R1, false); + + const double INVERTED_RATIO = 0.75; + double geomBoxWidthA_R2 = (availableWidthForSplit * INVERTED_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0); + double geomBoxWidthB_R2 = MONITOR_W - geomBoxWidthA_R2; + const int WIDTH2 = calculateFinalWidth(geomBoxWidthB_R2, false); + const int WIDTH_A_FINAL = calculateFinalWidth(geomBoxWidthA_R2, true); OK(getFromSocket("/keyword dwindle:default_split_ratio 1.25")); if (!spawnKitty("kitty_B")) return false; - NLog::log("{}Expecting kitty_B to take up roughly {}% of screen width", Colors::YELLOW, 100 - PERCENT); + NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH1, HEIGHT); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT)); OK(getFromSocket("/dispatch killwindow activewindow")); @@ -179,12 +197,12 @@ static bool test() { if (!spawnKitty("kitty_B")) return false; - NLog::log("{}Expecting kitty_B to take up roughly {}% of screen width", Colors::YELLOW, PERCENT); + NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH2, HEIGHT); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH2, HEIGHT)); OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - NLog::log("{}Expecting kitty_A to have the same width as the previous kitty_B", Colors::YELLOW); - EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT)); + NLog::log("{}Expecting kitty_A size: {},{}", Colors::YELLOW, WIDTH_A_FINAL, HEIGHT); + EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH_A_FINAL, HEIGHT)); OK(getFromSocket("/keyword dwindle:default_split_ratio 1")); } diff --git a/hyprtester/src/tests/main/workspaces.cpp b/hyprtester/src/tests/main/workspaces.cpp index de724af6..def35d08 100644 --- a/hyprtester/src/tests/main/workspaces.cpp +++ b/hyprtester/src/tests/main/workspaces.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include "../shared.hpp" @@ -14,6 +15,7 @@ static int ret = 0; using namespace Hyprutils::OS; using namespace Hyprutils::Memory; +using namespace Hyprutils::Utils; #define UP CUniquePointer #define SP CSharedPointer @@ -359,6 +361,95 @@ static bool test() { NLog::log("{}Killing all windows", Colors::YELLOW); Tests::killAllWindows(); + NLog::log("{}Testing asymmetric gap splits", Colors::YELLOW); + { + + CScopeGuard guard = {[&]() { + NLog::log("{}Cleaning up asymmetric gap test", Colors::YELLOW); + Tests::killAllWindows(); + OK(getFromSocket("/reload")); + }}; + + OK(getFromSocket("/dispatch workspace name:gap_split_test")); + OK(getFromSocket("r/keyword general:gaps_in 0")); + OK(getFromSocket("r/keyword general:border_size 0")); + OK(getFromSocket("r/keyword dwindle:split_width_multiplier 1.0")); + OK(getFromSocket("r/keyword workspace name:gap_split_test,gapsout:0 1000 0 0")); + + NLog::log("{}Testing default split (force_split = 0)", Colors::YELLOW); + OK(getFromSocket("r/keyword dwindle:force_split 0")); + + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) { + return false; + } + + NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + + Tests::killAllWindows(); + EXPECT(Tests::windowCount(), 0); + + NLog::log("{}Testing force_split = 1", Colors::YELLOW); + OK(getFromSocket("r/keyword dwindle:force_split 1")); + + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) { + return false; + } + + NLog::log("{}Expecting vertical split (B above A)", Colors::YELLOW); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + + NLog::log("{}Expecting horizontal split (C left of B)", Colors::YELLOW); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + + if (!Tests::spawnKitty("gaps_kitty_C")) { + return false; + } + + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); + + Tests::killAllWindows(); + EXPECT(Tests::windowCount(), 0); + + NLog::log("{}Testing force_split = 2", Colors::YELLOW); + OK(getFromSocket("r/keyword dwindle:force_split 2")); + + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) { + return false; + } + + NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + + NLog::log("{}Expecting horizontal split (C right of A)", Colors::YELLOW); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + + if (!Tests::spawnKitty("gaps_kitty_C")) { + return false; + } + + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); + } + + // kill all + NLog::log("{}Killing all windows", Colors::YELLOW); + Tests::killAllWindows(); + NLog::log("{}Expecting 0 windows", Colors::YELLOW); EXPECT(Tests::windowCount(), 0); diff --git a/src/layout/DwindleLayout.cpp b/src/layout/DwindleLayout.cpp index f5f545b1..4856c355 100644 --- a/src/layout/DwindleLayout.cpp +++ b/src/layout/DwindleLayout.cpp @@ -8,14 +8,51 @@ #include "../managers/LayoutManager.hpp" #include "../managers/EventManager.hpp" +SWorkspaceGaps CHyprDwindleLayout::getWorkspaceGaps(const PHLWORKSPACE& pWorkspace) { + const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(pWorkspace); + static auto PGAPSINDATA = CConfigValue("general:gaps_in"); + static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); + auto* const PGAPSIN = sc((PGAPSINDATA.ptr())->getData()); + auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())->getData()); + + SWorkspaceGaps gaps; + gaps.in = WORKSPACERULE.gapsIn.value_or(*PGAPSIN); + gaps.out = WORKSPACERULE.gapsOut.value_or(*PGAPSOUT); + return gaps; +} + +SNodeDisplayEdgeFlags CHyprDwindleLayout::getNodeDisplayEdgeFlags(const CBox& box, const PHLMONITOR& monitor) { + return { + .top = STICKS(box.y, monitor->m_position.y + monitor->m_reservedTopLeft.y), + .bottom = STICKS(box.y + box.h, monitor->m_position.y + monitor->m_size.y - monitor->m_reservedBottomRight.y), + .left = STICKS(box.x, monitor->m_position.x + monitor->m_reservedTopLeft.x), + .right = STICKS(box.x + box.w, monitor->m_position.x + monitor->m_size.x - monitor->m_reservedBottomRight.x), + }; +} + void SDwindleNodeData::recalcSizePosRecursive(bool force, bool horizontalOverride, bool verticalOverride) { if (children[0]) { static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); static auto PPRESERVESPLIT = CConfigValue("dwindle:preserve_split"); static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); + const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(workspaceID); + if (!PWORKSPACE) + return; + + const auto PMONITOR = PWORKSPACE->m_monitor.lock(); + if (!PMONITOR) + return; + + const auto edges = layout->getNodeDisplayEdgeFlags(box, PMONITOR); + auto [gapsIn, gapsOut] = layout->getWorkspaceGaps(PWORKSPACE); + + const Vector2D availableSize = box.size() - + Vector2D{(edges.left ? gapsOut.m_left : gapsIn.m_left / 2.f) + (edges.right ? gapsOut.m_right : gapsIn.m_right / 2.f), + (edges.top ? gapsOut.m_top : gapsIn.m_top / 2.f) + (edges.bottom ? gapsOut.m_bottom : gapsIn.m_bottom / 2.f)}; + if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0) - splitTop = box.h * *PFLMULT > box.w; + splitTop = availableSize.y * *PFLMULT > availableSize.x; if (verticalOverride) splitTop = true; @@ -26,14 +63,28 @@ void SDwindleNodeData::recalcSizePosRecursive(bool force, bool horizontalOverrid if (SPLITSIDE) { // split left/right - const float FIRSTSIZE = box.w / 2.0 * splitRatio; - children[0]->box = CBox{box.x, box.y, FIRSTSIZE, box.h}.noNegativeSize(); - children[1]->box = CBox{box.x + FIRSTSIZE, box.y, box.w - FIRSTSIZE, box.h}.noNegativeSize(); + const float gapsAppliedToChild1 = (edges.left ? gapsOut.m_left : gapsIn.m_left / 2.f) + gapsIn.m_right / 2.f; + const float gapsAppliedToChild2 = gapsIn.m_left / 2.f + (edges.right ? gapsOut.m_right : gapsIn.m_right / 2.f); + const float totalGaps = gapsAppliedToChild1 + gapsAppliedToChild2; + const float totalAvailable = box.w - totalGaps; + + const float child1Available = totalAvailable * (splitRatio / 2.f); + const float FIRSTSIZE = child1Available + gapsAppliedToChild1; + + children[0]->box = CBox{box.x, box.y, FIRSTSIZE, box.h}.noNegativeSize(); + children[1]->box = CBox{box.x + FIRSTSIZE, box.y, box.w - FIRSTSIZE, box.h}.noNegativeSize(); } else { // split top/bottom - const float FIRSTSIZE = box.h / 2.0 * splitRatio; - children[0]->box = CBox{box.x, box.y, box.w, FIRSTSIZE}.noNegativeSize(); - children[1]->box = CBox{box.x, box.y + FIRSTSIZE, box.w, box.h - FIRSTSIZE}.noNegativeSize(); + const float gapsAppliedToChild1 = (edges.top ? gapsOut.m_top : gapsIn.m_top / 2.f) + gapsIn.m_bottom / 2.f; + const float gapsAppliedToChild2 = gapsIn.m_top / 2.f + (edges.bottom ? gapsOut.m_bottom : gapsIn.m_bottom / 2.f); + const float totalGaps = gapsAppliedToChild1 + gapsAppliedToChild2; + const float totalAvailable = box.h - totalGaps; + + const float child1Available = totalAvailable * (splitRatio / 2.f); + const float FIRSTSIZE = child1Available + gapsAppliedToChild1; + + children[0]->box = CBox{box.x, box.y, box.w, FIRSTSIZE}.noNegativeSize(); + children[1]->box = CBox{box.x, box.y + FIRSTSIZE, box.w, box.h - FIRSTSIZE}.noNegativeSize(); } children[0]->recalcSizePosRecursive(force); @@ -115,10 +166,7 @@ void CHyprDwindleLayout::applyNodeDataToWindow(SDwindleNodeData* pNode, bool for } // for gaps outer - const bool DISPLAYLEFT = STICKS(pNode->box.x, PMONITOR->m_position.x + PMONITOR->m_reservedTopLeft.x); - const bool DISPLAYRIGHT = STICKS(pNode->box.x + pNode->box.w, PMONITOR->m_position.x + PMONITOR->m_size.x - PMONITOR->m_reservedBottomRight.x); - const bool DISPLAYTOP = STICKS(pNode->box.y, PMONITOR->m_position.y + PMONITOR->m_reservedTopLeft.y); - const bool DISPLAYBOTTOM = STICKS(pNode->box.y + pNode->box.h, PMONITOR->m_position.y + PMONITOR->m_size.y - PMONITOR->m_reservedBottomRight.y); + const auto edges = getNodeDisplayEdgeFlags(pNode->box, PMONITOR); const auto PWINDOW = pNode->pWindow.lock(); // get specific gaps and rules for this workspace, @@ -179,9 +227,9 @@ void CHyprDwindleLayout::applyNodeDataToWindow(SDwindleNodeData* pNode, bool for } } - const auto GAPOFFSETTOPLEFT = Vector2D(sc(DISPLAYLEFT ? gapsOut.m_left : gapsIn.m_left), sc(DISPLAYTOP ? gapsOut.m_top : gapsIn.m_top)); + const auto GAPOFFSETTOPLEFT = Vector2D(sc(edges.left ? gapsOut.m_left : gapsIn.m_left), sc(edges.top ? gapsOut.m_top : gapsIn.m_top)); - const auto GAPOFFSETBOTTOMRIGHT = Vector2D(sc(DISPLAYRIGHT ? gapsOut.m_right : gapsIn.m_right), sc(DISPLAYBOTTOM ? gapsOut.m_bottom : gapsIn.m_bottom)); + const auto GAPOFFSETBOTTOMRIGHT = Vector2D(sc(edges.right ? gapsOut.m_right : gapsIn.m_right), sc(edges.bottom ? gapsOut.m_bottom : gapsIn.m_bottom)); calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2; calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding; @@ -349,7 +397,6 @@ void CHyprDwindleLayout::onWindowCreatedTiling(PHLWINDOW pWindow, eDirection dir } // get the node under our cursor - m_dwindleNodesData.emplace_back(); const auto NEWPARENT = &m_dwindleNodesData.back(); @@ -362,8 +409,17 @@ void CHyprDwindleLayout::onWindowCreatedTiling(PHLWINDOW pWindow, eDirection dir static auto PWIDTHMULTIPLIER = CConfigValue("dwindle:split_width_multiplier"); + const auto edges = getNodeDisplayEdgeFlags(NEWPARENT->box, PMONITOR); + + const auto WORKSPACE = g_pCompositor->getWorkspaceByID(PNODE->workspaceID); + auto [gapsIn, gapsOut] = getWorkspaceGaps(WORKSPACE); + // if cursor over first child, make it first, etc - const auto SIDEBYSIDE = NEWPARENT->box.w > NEWPARENT->box.h * *PWIDTHMULTIPLIER; + const Vector2D availableSize = NEWPARENT->box.size() - + Vector2D{(edges.left ? gapsOut.m_left : gapsIn.m_left / 2.f) + (edges.right ? gapsOut.m_right : gapsIn.m_right / 2.f), + (edges.top ? gapsOut.m_top : gapsIn.m_top / 2.f) + (edges.bottom ? gapsOut.m_bottom : gapsIn.m_bottom / 2.f)}; + + const auto SIDEBYSIDE = availableSize.x > availableSize.y * *PWIDTHMULTIPLIER; NEWPARENT->splitTop = !SIDEBYSIDE; static auto PFORCESPLIT = CConfigValue("dwindle:force_split"); @@ -611,11 +667,8 @@ void CHyprDwindleLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorn static auto PSMARTRESIZING = CConfigValue("dwindle:smart_resizing"); // get some data about our window - const auto PMONITOR = PWINDOW->m_monitor.lock(); - const bool DISPLAYLEFT = STICKS(PWINDOW->m_position.x, PMONITOR->m_position.x + PMONITOR->m_reservedTopLeft.x); - const bool DISPLAYRIGHT = STICKS(PWINDOW->m_position.x + PWINDOW->m_size.x, PMONITOR->m_position.x + PMONITOR->m_size.x - PMONITOR->m_reservedBottomRight.x); - const bool DISPLAYTOP = STICKS(PWINDOW->m_position.y, PMONITOR->m_position.y + PMONITOR->m_reservedTopLeft.y); - const bool DISPLAYBOTTOM = STICKS(PWINDOW->m_position.y + PWINDOW->m_size.y, PMONITOR->m_position.y + PMONITOR->m_size.y - PMONITOR->m_reservedBottomRight.y); + const auto PMONITOR = PWINDOW->m_monitor.lock(); + const auto edges = getNodeDisplayEdgeFlags(CBox{PWINDOW->m_position, PWINDOW->m_size}, PMONITOR); if (PWINDOW->m_isPseudotiled) { if (!m_pseudoDragFlags.started) { @@ -663,10 +716,10 @@ void CHyprDwindleLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorn // construct allowed movement Vector2D allowedMovement = pixResize; - if (DISPLAYLEFT && DISPLAYRIGHT) + if (edges.left && edges.right) allowedMovement.x = 0; - if (DISPLAYBOTTOM && DISPLAYTOP) + if (edges.bottom && edges.top) allowedMovement.y = 0; if (*PSMARTRESIZING == 1) { @@ -676,10 +729,10 @@ void CHyprDwindleLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorn SDwindleNodeData* PHOUTER = nullptr; SDwindleNodeData* PHINNER = nullptr; - const auto LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT || DISPLAYRIGHT; - const auto TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT || DISPLAYBOTTOM; - const auto RIGHT = corner == CORNER_TOPRIGHT || corner == CORNER_BOTTOMRIGHT || DISPLAYLEFT; - const auto BOTTOM = corner == CORNER_BOTTOMLEFT || corner == CORNER_BOTTOMRIGHT || DISPLAYTOP; + const auto LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT || edges.right; + const auto TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT || edges.bottom; + const auto RIGHT = corner == CORNER_TOPRIGHT || corner == CORNER_BOTTOMRIGHT || edges.left; + const auto BOTTOM = corner == CORNER_BOTTOMLEFT || corner == CORNER_BOTTOMRIGHT || edges.top; const auto NONE = corner == CORNER_NONE; for (auto PCURRENT = PNODE; PCURRENT && PCURRENT->pParent; PCURRENT = PCURRENT->pParent) { diff --git a/src/layout/DwindleLayout.hpp b/src/layout/DwindleLayout.hpp index 23f19956..de80beed 100644 --- a/src/layout/DwindleLayout.hpp +++ b/src/layout/DwindleLayout.hpp @@ -1,6 +1,7 @@ #pragma once #include "IHyprLayout.hpp" +#include "../config/ConfigDataValues.hpp" #include "../desktop/DesktopTypes.hpp" #include @@ -12,6 +13,15 @@ class CHyprDwindleLayout; enum eFullscreenMode : int8_t; +struct SNodeDisplayEdgeFlags { + bool top = false, bottom = false, left = false, right = false; +}; + +struct SWorkspaceGaps { + CCssGapData in; + CCssGapData out; +}; + struct SDwindleNodeData { SDwindleNodeData* pParent = nullptr; bool isNode = false; @@ -65,6 +75,9 @@ class CHyprDwindleLayout : public IHyprLayout { virtual void onDisable(); private: + SWorkspaceGaps getWorkspaceGaps(const PHLWORKSPACE& pWorkspace); + SNodeDisplayEdgeFlags getNodeDisplayEdgeFlags(const CBox& box, const PHLMONITOR& monitor); + std::list m_dwindleNodesData; struct {