3
0
mirror of https://github.com/hyprwm/Hyprland.git synced 2025-10-29 11:22:47 +00:00

dwindle: rework split logic to be fully gap-aware (#12047)

This commit is contained in:
crossatko 2025-10-24 20:01:05 +02:00 committed by GitHub
parent aa5a239ac9
commit 151b5f6978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 214 additions and 39 deletions

View File

@ -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"));
}

View File

@ -6,6 +6,7 @@
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <csignal>
#include <cerrno>
#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);

View File

@ -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<Hyprlang::CUSTOMTYPE>("general:gaps_in");
static auto PGAPSOUTDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_out");
auto* const PGAPSIN = sc<CCssGapData*>((PGAPSINDATA.ptr())->getData());
auto* const PGAPSOUT = sc<CCssGapData*>((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<Hyprlang::INT>("dwindle:smart_split");
static auto PPRESERVESPLIT = CConfigValue<Hyprlang::INT>("dwindle:preserve_split");
static auto PFLMULT = CConfigValue<Hyprlang::FLOAT>("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<double>(DISPLAYLEFT ? gapsOut.m_left : gapsIn.m_left), sc<double>(DISPLAYTOP ? gapsOut.m_top : gapsIn.m_top));
const auto GAPOFFSETTOPLEFT = Vector2D(sc<double>(edges.left ? gapsOut.m_left : gapsIn.m_left), sc<double>(edges.top ? gapsOut.m_top : gapsIn.m_top));
const auto GAPOFFSETBOTTOMRIGHT = Vector2D(sc<double>(DISPLAYRIGHT ? gapsOut.m_right : gapsIn.m_right), sc<double>(DISPLAYBOTTOM ? gapsOut.m_bottom : gapsIn.m_bottom));
const auto GAPOFFSETBOTTOMRIGHT = Vector2D(sc<double>(edges.right ? gapsOut.m_right : gapsIn.m_right), sc<double>(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<Hyprlang::FLOAT>("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<Hyprlang::INT>("dwindle:force_split");
@ -611,11 +667,8 @@ void CHyprDwindleLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorn
static auto PSMARTRESIZING = CConfigValue<Hyprlang::INT>("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) {

View File

@ -1,6 +1,7 @@
#pragma once
#include "IHyprLayout.hpp"
#include "../config/ConfigDataValues.hpp"
#include "../desktop/DesktopTypes.hpp"
#include <list>
@ -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<SDwindleNodeData> m_dwindleNodesData;
struct {