From 723870337f299d4c80aa0048144f3a1ca1c885bd Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:30:39 +0000 Subject: layout: rethonk layouts from the ground up (#12890) Rewrites layouts to be much smaller, and deal with much less annoying BS. Improves the overall architecture, unifies handling of pseudotiling, and various other improvements. --- src/layout/DwindleLayout.cpp | 1190 --------------- src/layout/DwindleLayout.hpp | 110 -- src/layout/IHyprLayout.cpp | 1062 -------------- src/layout/IHyprLayout.hpp | 248 ---- src/layout/LayoutManager.cpp | 344 +++++ src/layout/LayoutManager.hpp | 86 ++ src/layout/MasterLayout.cpp | 1526 -------------------- src/layout/MasterLayout.hpp | 112 -- src/layout/algorithm/Algorithm.cpp | 264 ++++ src/layout/algorithm/Algorithm.hpp | 64 + src/layout/algorithm/FloatingAlgorithm.cpp | 18 + src/layout/algorithm/FloatingAlgorithm.hpp | 31 + src/layout/algorithm/ModeAlgorithm.cpp | 11 + src/layout/algorithm/ModeAlgorithm.hpp | 54 + src/layout/algorithm/TiledAlgorithm.hpp | 26 + .../floating/default/DefaultFloatingAlgorithm.cpp | 223 +++ .../floating/default/DefaultFloatingAlgorithm.hpp | 26 + .../algorithm/tiled/dwindle/DwindleAlgorithm.cpp | 772 ++++++++++ .../algorithm/tiled/dwindle/DwindleAlgorithm.hpp | 57 + .../algorithm/tiled/master/MasterAlgorithm.cpp | 1292 +++++++++++++++++ .../algorithm/tiled/master/MasterAlgorithm.hpp | 75 + .../algorithm/tiled/monocle/MonocleAlgorithm.cpp | 274 ++++ .../algorithm/tiled/monocle/MonocleAlgorithm.hpp | 52 + .../tiled/scrolling/ScrollTapeController.cpp | 293 ++++ .../tiled/scrolling/ScrollTapeController.hpp | 83 ++ .../tiled/scrolling/ScrollingAlgorithm.cpp | 1412 ++++++++++++++++++ .../tiled/scrolling/ScrollingAlgorithm.hpp | 137 ++ src/layout/space/Space.cpp | 185 +++ src/layout/space/Space.hpp | 67 + src/layout/supplementary/DragController.cpp | 396 +++++ src/layout/supplementary/DragController.hpp | 54 + src/layout/supplementary/WorkspaceAlgoMatcher.cpp | 139 ++ src/layout/supplementary/WorkspaceAlgoMatcher.hpp | 46 + src/layout/target/Target.cpp | 146 ++ src/layout/target/Target.hpp | 79 + src/layout/target/WindowGroupTarget.cpp | 92 ++ src/layout/target/WindowGroupTarget.hpp | 39 + src/layout/target/WindowTarget.cpp | 363 +++++ src/layout/target/WindowTarget.hpp | 40 + 39 files changed, 7240 insertions(+), 4248 deletions(-) delete mode 100644 src/layout/DwindleLayout.cpp delete mode 100644 src/layout/DwindleLayout.hpp delete mode 100644 src/layout/IHyprLayout.cpp delete mode 100644 src/layout/IHyprLayout.hpp create mode 100644 src/layout/LayoutManager.cpp create mode 100644 src/layout/LayoutManager.hpp delete mode 100644 src/layout/MasterLayout.cpp delete mode 100644 src/layout/MasterLayout.hpp create mode 100644 src/layout/algorithm/Algorithm.cpp create mode 100644 src/layout/algorithm/Algorithm.hpp create mode 100644 src/layout/algorithm/FloatingAlgorithm.cpp create mode 100644 src/layout/algorithm/FloatingAlgorithm.hpp create mode 100644 src/layout/algorithm/ModeAlgorithm.cpp create mode 100644 src/layout/algorithm/ModeAlgorithm.hpp create mode 100644 src/layout/algorithm/TiledAlgorithm.hpp create mode 100644 src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp create mode 100644 src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.hpp create mode 100644 src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp create mode 100644 src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp create mode 100644 src/layout/algorithm/tiled/master/MasterAlgorithm.cpp create mode 100644 src/layout/algorithm/tiled/master/MasterAlgorithm.hpp create mode 100644 src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp create mode 100644 src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp create mode 100644 src/layout/algorithm/tiled/scrolling/ScrollTapeController.cpp create mode 100644 src/layout/algorithm/tiled/scrolling/ScrollTapeController.hpp create mode 100644 src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp create mode 100644 src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp create mode 100644 src/layout/space/Space.cpp create mode 100644 src/layout/space/Space.hpp create mode 100644 src/layout/supplementary/DragController.cpp create mode 100644 src/layout/supplementary/DragController.hpp create mode 100644 src/layout/supplementary/WorkspaceAlgoMatcher.cpp create mode 100644 src/layout/supplementary/WorkspaceAlgoMatcher.hpp create mode 100644 src/layout/target/Target.cpp create mode 100644 src/layout/target/Target.hpp create mode 100644 src/layout/target/WindowGroupTarget.cpp create mode 100644 src/layout/target/WindowGroupTarget.hpp create mode 100644 src/layout/target/WindowTarget.cpp create mode 100644 src/layout/target/WindowTarget.hpp (limited to 'src/layout') diff --git a/src/layout/DwindleLayout.cpp b/src/layout/DwindleLayout.cpp deleted file mode 100644 index 70d052ea..00000000 --- a/src/layout/DwindleLayout.cpp +++ /dev/null @@ -1,1190 +0,0 @@ -#include "DwindleLayout.hpp" -#include "../Compositor.hpp" -#include "../config/ConfigValue.hpp" -#include "../config/ConfigManager.hpp" -#include "../render/decorations/CHyprGroupBarDecoration.hpp" -#include "../render/Renderer.hpp" -#include "../managers/input/InputManager.hpp" -#include "../managers/LayoutManager.hpp" -#include "../managers/EventManager.hpp" -#include "../desktop/state/FocusState.hpp" -#include "xwayland/XWayland.hpp" - -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"); - - if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0) - splitTop = box.h * *PFLMULT > box.w; - - if (verticalOverride) - splitTop = true; - else if (horizontalOverride) - splitTop = false; - - const auto SPLITSIDE = !splitTop; - - 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(); - } 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(); - } - - children[0]->recalcSizePosRecursive(force); - children[1]->recalcSizePosRecursive(force); - } else { - layout->applyNodeDataToWindow(self.lock(), force); - } -} - -void SDwindleNodeData::applyRootBox() { - box = layout->workAreaOnWorkspace(g_pCompositor->getWorkspaceByID(workspaceID)); -} - -int CHyprDwindleLayout::getNodesOnWorkspace(const WORKSPACEID& id) { - int no = 0; - for (auto const& n : m_dwindleNodesData) { - if (n->workspaceID == id && n->valid) - ++no; - } - return no; -} - -SP CHyprDwindleLayout::getFirstNodeOnWorkspace(const WORKSPACEID& id) { - for (auto& n : m_dwindleNodesData) { - if (n->workspaceID == id && validMapped(n->pWindow)) - return n; - } - return nullptr; -} - -SP CHyprDwindleLayout::getClosestNodeOnWorkspace(const WORKSPACEID& id, const Vector2D& point) { - SP res = nullptr; - double distClosest = -1; - for (auto& n : m_dwindleNodesData) { - if (n->workspaceID == id && validMapped(n->pWindow)) { - auto distAnother = vecToRectDistanceSquared(point, n->box.pos(), n->box.pos() + n->box.size()); - if (!res || distAnother < distClosest) { - res = n; - distClosest = distAnother; - } - } - } - return res; -} - -SP CHyprDwindleLayout::getNodeFromWindow(PHLWINDOW pWindow) { - for (auto& n : m_dwindleNodesData) { - if (n->pWindow.lock() == pWindow && !n->isNode) - return n; - } - - return nullptr; -} - -SP CHyprDwindleLayout::getMasterNodeOnWorkspace(const WORKSPACEID& id) { - for (auto& n : m_dwindleNodesData) { - if (!n->pParent && n->workspaceID == id) - return n; - } - return nullptr; -} - -void CHyprDwindleLayout::applyNodeDataToWindow(SP pNode, bool force) { - // Don't set nodes, only windows. - if (pNode->isNode) - return; - - PHLMONITOR PMONITOR = nullptr; - - const auto WS = g_pCompositor->getWorkspaceByID(pNode->workspaceID); - - if (g_pCompositor->isWorkspaceSpecial(pNode->workspaceID)) { - for (auto const& m : g_pCompositor->m_monitors) { - if (m->activeSpecialWorkspaceID() == pNode->workspaceID) { - PMONITOR = m; - break; - } - } - } else if (WS) - PMONITOR = WS->m_monitor.lock(); - - if (!PMONITOR || !WS) { - Log::logger->log(Log::ERR, "Orphaned Node {}!!", pNode); - return; - } - - // for gaps outer - const auto MONITOR_WORKAREA = workAreaOnWorkspace(WS); - const bool DISPLAYLEFT = STICKS(pNode->box.x, MONITOR_WORKAREA.x); - const bool DISPLAYRIGHT = STICKS(pNode->box.x + pNode->box.w, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); - const bool DISPLAYTOP = STICKS(pNode->box.y, MONITOR_WORKAREA.y); - const bool DISPLAYBOTTOM = STICKS(pNode->box.y + pNode->box.h, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h); - - const auto PWINDOW = pNode->pWindow.lock(); - // get specific gaps and rules for this workspace, - // if user specified them in config - const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(g_pCompositor->getWorkspaceByID(pNode->workspaceID)); - - if (!validMapped(PWINDOW)) { - Log::logger->log(Log::ERR, "Node {} holding invalid {}!!", pNode, PWINDOW); - onWindowRemovedTiling(PWINDOW); - return; - } - - if (PWINDOW->isFullscreen() && !pNode->ignoreFullscreenChecks) - return; - - PWINDOW->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - PWINDOW->updateWindowData(); - - static auto PGAPSINDATA = CConfigValue("general:gaps_in"); - auto* const PGAPSIN = sc((PGAPSINDATA.ptr())->getData()); - - auto gapsIn = WORKSPACERULE.gapsIn.value_or(*PGAPSIN); - CBox nodeBox = pNode->box; - nodeBox.round(); - - PWINDOW->m_size = nodeBox.size(); - PWINDOW->m_position = nodeBox.pos(); - - PWINDOW->updateWindowDecos(); - - auto calcPos = PWINDOW->m_position; - auto calcSize = PWINDOW->m_size; - - const static auto REQUESTEDRATIO = CConfigValue("dwindle:single_window_aspect_ratio"); - const static auto REQUESTEDRATIOTOLERANCE = CConfigValue("dwindle:single_window_aspect_ratio_tolerance"); - - Vector2D ratioPadding; - - if ((*REQUESTEDRATIO).y != 0 && !pNode->pParent) { - const Vector2D originalSize = MONITOR_WORKAREA.size(); - - const double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y; - const double originalRatio = originalSize.x / originalSize.y; - - if (requestedRatio > originalRatio) { - double padding = originalSize.y - (originalSize.x / requestedRatio); - - if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.y) - ratioPadding = Vector2D{0., padding}; - } else if (requestedRatio < originalRatio) { - double padding = originalSize.x - (originalSize.y * requestedRatio); - - if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.x) - ratioPadding = Vector2D{padding, 0.}; - } - } - - const auto GAPOFFSETTOPLEFT = Vector2D(sc(DISPLAYLEFT ? 0 : gapsIn.m_left), sc(DISPLAYTOP ? 0 : gapsIn.m_top)); - - const auto GAPOFFSETBOTTOMRIGHT = Vector2D(sc(DISPLAYRIGHT ? 0 : gapsIn.m_right), sc(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom)); - - calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2; - calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding; - - if (PWINDOW->m_isPseudotiled) { - // Calculate pseudo - float scale = 1; - - // adjust if doesn't fit - if (PWINDOW->m_pseudoSize.x > calcSize.x || PWINDOW->m_pseudoSize.y > calcSize.y) { - if (PWINDOW->m_pseudoSize.x > calcSize.x) { - scale = calcSize.x / PWINDOW->m_pseudoSize.x; - } - - if (PWINDOW->m_pseudoSize.y * scale > calcSize.y) { - scale = calcSize.y / PWINDOW->m_pseudoSize.y; - } - - auto DELTA = calcSize - PWINDOW->m_pseudoSize * scale; - calcSize = PWINDOW->m_pseudoSize * scale; - calcPos = calcPos + DELTA / 2.f; // center - } else { - auto DELTA = calcSize - PWINDOW->m_pseudoSize; - calcPos = calcPos + DELTA / 2.f; // center - calcSize = PWINDOW->m_pseudoSize; - } - } - - const auto RESERVED = PWINDOW->getFullWindowReservedArea(); - calcPos = calcPos + RESERVED.topLeft; - calcSize = calcSize - (RESERVED.topLeft + RESERVED.bottomRight); - - Vector2D availableSpace = calcSize; - - static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); - - if (*PCLAMP_TILED) { - const auto borderSize = PWINDOW->getRealBorderSize(); - Vector2D monitorAvailable = MONITOR_WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize}; - - Vector2D minSize = PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}).clamp(Vector2D{0, 0}, monitorAvailable); - Vector2D maxSize = PWINDOW->isFullscreen() ? Vector2D{INFINITY, INFINITY} : - PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}).clamp(Vector2D{0, 0}, monitorAvailable); - calcSize = calcSize.clamp(minSize, maxSize); - - calcPos += (availableSpace - calcSize) / 2.0; - - calcPos.x = std::clamp(calcPos.x, MONITOR_WORKAREA.x + borderSize, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w - calcSize.x - borderSize); - calcPos.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y + borderSize, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y - borderSize); - } - - if (PWINDOW->onSpecialWorkspace() && !PWINDOW->isFullscreen()) { - // if special, we adjust the coords a bit - static auto PSCALEFACTOR = CConfigValue("dwindle:special_scale_factor"); - - CBox wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR}; - wb.round(); // avoid rounding mess - - *PWINDOW->m_realPosition = wb.pos(); - *PWINDOW->m_realSize = wb.size(); - } else { - CBox wb = {calcPos, calcSize}; - wb.round(); // avoid rounding mess - - *PWINDOW->m_realSize = wb.size(); - *PWINDOW->m_realPosition = wb.pos(); - } - - if (force) { - g_pHyprRenderer->damageWindow(PWINDOW); - - PWINDOW->m_realPosition->warp(); - PWINDOW->m_realSize->warp(); - - g_pHyprRenderer->damageWindow(PWINDOW); - } - - PWINDOW->updateWindowDecos(); -} - -void CHyprDwindleLayout::onWindowCreatedTiling(PHLWINDOW pWindow, eDirection direction) { - if (pWindow->m_isFloating) - return; - - const auto PNODE = m_dwindleNodesData.emplace_back(makeShared()); - PNODE->self = PNODE; - - const auto PMONITOR = pWindow->m_monitor.lock(); - - static auto PUSEACTIVE = CConfigValue("dwindle:use_active_for_splits"); - static auto PDEFAULTSPLIT = CConfigValue("dwindle:default_split_ratio"); - - if (direction != DIRECTION_DEFAULT && m_overrideDirection == DIRECTION_DEFAULT) - m_overrideDirection = direction; - - // Populate the node with our window's data - PNODE->workspaceID = pWindow->workspaceID(); - PNODE->pWindow = pWindow; - PNODE->isNode = false; - PNODE->layout = this; - - SP OPENINGON; - - const auto MOUSECOORDS = m_overrideFocalPoint.value_or(g_pInputManager->getMouseCoordsInternal()); - const auto MONFROMCURSOR = g_pCompositor->getMonitorFromVector(MOUSECOORDS); - - if (PMONITOR->m_id == MONFROMCURSOR->m_id && - (PNODE->workspaceID == PMONITOR->activeWorkspaceID() || (g_pCompositor->isWorkspaceSpecial(PNODE->workspaceID) && PMONITOR->m_activeSpecialWorkspace)) && !*PUSEACTIVE) { - OPENINGON = getNodeFromWindow( - g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::SKIP_FULLSCREEN_PRIORITY)); - - if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, PMONITOR)) - OPENINGON = getClosestNodeOnWorkspace(PNODE->workspaceID, MOUSECOORDS); - - } else if (*PUSEACTIVE) { - if (Desktop::focusState()->window() && !Desktop::focusState()->window()->m_isFloating && Desktop::focusState()->window() != pWindow && - Desktop::focusState()->window()->m_workspace == pWindow->m_workspace && Desktop::focusState()->window()->m_isMapped) { - OPENINGON = getNodeFromWindow(Desktop::focusState()->window()); - } else { - OPENINGON = getNodeFromWindow(g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS)); - } - - if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, PMONITOR)) - OPENINGON = getClosestNodeOnWorkspace(PNODE->workspaceID, MOUSECOORDS); - - } else - OPENINGON = getFirstNodeOnWorkspace(pWindow->workspaceID()); - - Log::logger->log(Log::DEBUG, "OPENINGON: {}, Monitor: {}", OPENINGON, PMONITOR->m_id); - - if (OPENINGON && OPENINGON->workspaceID != PNODE->workspaceID) { - // special workspace handling - OPENINGON = getFirstNodeOnWorkspace(PNODE->workspaceID); - } - - // first, check if OPENINGON isn't too big. - const auto PREDSIZEMAX = OPENINGON ? Vector2D(OPENINGON->box.w, OPENINGON->box.h) : PMONITOR->m_size; - if (const auto MAXSIZE = pWindow->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PREDSIZEMAX.x || MAXSIZE.y < PREDSIZEMAX.y) { - // we can't continue. make it floating. - pWindow->m_isFloating = true; - std::erase(m_dwindleNodesData, PNODE); - g_pLayoutManager->getCurrentLayout()->onWindowCreatedFloating(pWindow); - return; - } - - // last fail-safe to avoid duplicate fullscreens - if ((!OPENINGON || OPENINGON->pWindow.lock() == pWindow) && getNodesOnWorkspace(PNODE->workspaceID) > 1) { - for (auto& node : m_dwindleNodesData) { - if (node->workspaceID == PNODE->workspaceID && node->pWindow.lock() && node->pWindow.lock() != pWindow) { - OPENINGON = node; - break; - } - } - } - - // if it's the first, it's easy. Make it fullscreen. - if (!OPENINGON || OPENINGON->pWindow.lock() == pWindow) { - PNODE->applyRootBox(); - applyNodeDataToWindow(PNODE); - return; - } - - // get the node under our cursor - - const auto NEWPARENT = m_dwindleNodesData.emplace_back(makeShared()); - - // make the parent have the OPENINGON's stats - NEWPARENT->box = OPENINGON->box; - NEWPARENT->workspaceID = OPENINGON->workspaceID; - NEWPARENT->pParent = OPENINGON->pParent; - NEWPARENT->isNode = true; // it is a node - NEWPARENT->splitRatio = std::clamp(*PDEFAULTSPLIT, 0.1f, 1.9f); - NEWPARENT->layout = this; - - static auto PWIDTHMULTIPLIER = CConfigValue("dwindle:split_width_multiplier"); - - // if cursor over first child, make it first, etc - const auto SIDEBYSIDE = NEWPARENT->box.w > NEWPARENT->box.h * *PWIDTHMULTIPLIER; - NEWPARENT->splitTop = !SIDEBYSIDE; - - static auto PFORCESPLIT = CConfigValue("dwindle:force_split"); - static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue("dwindle:permanent_direction_override"); - static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); - static auto PSPLITBIAS = CConfigValue("dwindle:split_bias"); - - bool horizontalOverride = false; - bool verticalOverride = false; - - // let user select position -> top, right, bottom, left - if (m_overrideDirection != DIRECTION_DEFAULT) { - - // this is horizontal - if (m_overrideDirection % 2 == 0) - verticalOverride = true; - else - horizontalOverride = true; - - // 0 -> top and left | 1,2 -> right and bottom - if (m_overrideDirection % 3 == 0) { - NEWPARENT->children[1] = OPENINGON; - NEWPARENT->children[0] = PNODE; - } else { - NEWPARENT->children[0] = OPENINGON; - NEWPARENT->children[1] = PNODE; - } - - // whether or not the override persists after opening one window - if (*PERMANENTDIRECTIONOVERRIDE == 0) - m_overrideDirection = DIRECTION_DEFAULT; - } else if (*PSMARTSPLIT == 1) { - const auto PARENT_CENTER = NEWPARENT->box.pos() + NEWPARENT->box.size() / 2; - const auto PARENT_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w; - const auto DELTA = MOUSECOORDS - PARENT_CENTER; - const auto DELTA_SLOPE = DELTA.y / DELTA.x; - - if (abs(DELTA_SLOPE) < PARENT_PROPORTIONS) { - if (DELTA.x > 0) { - // right - NEWPARENT->splitTop = false; - NEWPARENT->children[0] = OPENINGON; - NEWPARENT->children[1] = PNODE; - } else { - // left - NEWPARENT->splitTop = false; - NEWPARENT->children[0] = PNODE; - NEWPARENT->children[1] = OPENINGON; - } - } else { - if (DELTA.y > 0) { - // bottom - NEWPARENT->splitTop = true; - NEWPARENT->children[0] = OPENINGON; - NEWPARENT->children[1] = PNODE; - } else { - // top - NEWPARENT->splitTop = true; - NEWPARENT->children[0] = PNODE; - NEWPARENT->children[1] = OPENINGON; - } - } - } else if (*PFORCESPLIT == 0 || !pWindow->m_firstMap) { - if ((SIDEBYSIDE && - VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y + NEWPARENT->box.h)) || - (!SIDEBYSIDE && - VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w, NEWPARENT->box.y + NEWPARENT->box.h / 2.f))) { - // we are hovering over the first node, make PNODE first. - NEWPARENT->children[1] = OPENINGON; - NEWPARENT->children[0] = PNODE; - } else { - // we are hovering over the second node, make PNODE second. - NEWPARENT->children[0] = OPENINGON; - NEWPARENT->children[1] = PNODE; - } - } else { - if (*PFORCESPLIT == 1) { - NEWPARENT->children[1] = OPENINGON; - NEWPARENT->children[0] = PNODE; - } else { - NEWPARENT->children[0] = OPENINGON; - NEWPARENT->children[1] = PNODE; - } - } - - // split in favor of a specific window - if (*PSPLITBIAS && NEWPARENT->children[0] == PNODE) - NEWPARENT->splitRatio = 2.f - NEWPARENT->splitRatio; - - // and update the previous parent if it exists - if (OPENINGON->pParent) { - if (OPENINGON->pParent->children[0] == OPENINGON) { - OPENINGON->pParent->children[0] = NEWPARENT; - } else { - OPENINGON->pParent->children[1] = NEWPARENT; - } - } - - // Update the children - if (!verticalOverride && (NEWPARENT->box.w * *PWIDTHMULTIPLIER > NEWPARENT->box.h || horizontalOverride)) { - // split left/right -> forced - OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)}; - PNODE->box = {Vector2D(NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)}; - } else { - // split top/bottom - OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)}; - PNODE->box = {Vector2D(NEWPARENT->box.x, NEWPARENT->box.y + NEWPARENT->box.h / 2.f), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)}; - } - - OPENINGON->pParent = NEWPARENT; - PNODE->pParent = NEWPARENT; - - NEWPARENT->recalcSizePosRecursive(false, horizontalOverride, verticalOverride); - - recalculateMonitor(pWindow->monitorID()); - pWindow->m_workspace->updateWindows(); -} - -void CHyprDwindleLayout::onWindowRemovedTiling(PHLWINDOW pWindow) { - - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE) { - Log::logger->log(Log::ERR, "onWindowRemovedTiling node null?"); - return; - } - - pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - pWindow->updateWindowData(); - - if (pWindow->isFullscreen()) - g_pCompositor->setWindowFullscreenInternal(pWindow, FSMODE_NONE); - - const auto PPARENT = PNODE->pParent; - - if (!PPARENT) { - Log::logger->log(Log::DEBUG, "Removing last node (dwindle)"); - std::erase(m_dwindleNodesData, PNODE); - return; - } - - const auto PSIBLING = PPARENT->children[0] == PNODE ? PPARENT->children[1] : PPARENT->children[0]; - - PSIBLING->box = PPARENT->box; - PSIBLING->pParent = PPARENT->pParent; - - if (PPARENT->pParent != nullptr) { - if (PPARENT->pParent->children[0] == PPARENT) { - PPARENT->pParent->children[0] = PSIBLING; - } else { - PPARENT->pParent->children[1] = PSIBLING; - } - } - - PPARENT->valid = false; - PNODE->valid = false; - - if (PSIBLING->pParent) - PSIBLING->pParent->recalcSizePosRecursive(); - else - PSIBLING->recalcSizePosRecursive(); - - std::erase(m_dwindleNodesData, PPARENT); - std::erase(m_dwindleNodesData, PNODE); - pWindow->m_workspace->updateWindows(); -} - -void CHyprDwindleLayout::recalculateMonitor(const MONITORID& monid) { - const auto PMONITOR = g_pCompositor->getMonitorFromID(monid); - - if (!PMONITOR || !PMONITOR->m_activeWorkspace) - return; // ??? - - g_pHyprRenderer->damageMonitor(PMONITOR); - - if (PMONITOR->m_activeSpecialWorkspace) - calculateWorkspace(PMONITOR->m_activeSpecialWorkspace); - - calculateWorkspace(PMONITOR->m_activeWorkspace); - -#ifndef NO_XWAYLAND - CBox box = g_pCompositor->calculateX11WorkArea(); - if (!g_pXWayland || !g_pXWayland->m_wm) - return; - g_pXWayland->m_wm->updateWorkArea(box.x, box.y, box.w, box.h); -#endif -} - -void CHyprDwindleLayout::calculateWorkspace(const PHLWORKSPACE& pWorkspace) { - const auto PMONITOR = pWorkspace->m_monitor.lock(); - - if (!PMONITOR) - return; - - if (pWorkspace->m_hasFullscreenWindow) { - // massive hack from the fullscreen func - const auto PFULLWINDOW = pWorkspace->getFullscreenWindow(); - - if (pWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN) { - *PFULLWINDOW->m_realPosition = PMONITOR->m_position; - *PFULLWINDOW->m_realSize = PMONITOR->m_size; - } else if (pWorkspace->m_fullscreenMode == FSMODE_MAXIMIZED) { - SP fakeNode = makeShared(); - fakeNode->self = fakeNode; - fakeNode->pWindow = PFULLWINDOW; - fakeNode->box = workAreaOnWorkspace(pWorkspace); - fakeNode->workspaceID = pWorkspace->m_id; - PFULLWINDOW->m_position = fakeNode->box.pos(); - PFULLWINDOW->m_size = fakeNode->box.size(); - fakeNode->ignoreFullscreenChecks = true; - fakeNode->layout = this; - - applyNodeDataToWindow(fakeNode); - } - - // if has fullscreen, don't calculate the rest - return; - } - - const auto TOPNODE = getMasterNodeOnWorkspace(pWorkspace->m_id); - - if (TOPNODE) { - TOPNODE->applyRootBox(); - TOPNODE->recalcSizePosRecursive(); - } -} - -bool CHyprDwindleLayout::isWindowTiled(PHLWINDOW pWindow) { - return getNodeFromWindow(pWindow) != nullptr; -} - -void CHyprDwindleLayout::onBeginDragWindow() { - m_pseudoDragFlags.started = false; - m_pseudoDragFlags.pseudo = false; - IHyprLayout::onBeginDragWindow(); -} - -void CHyprDwindleLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorner corner, PHLWINDOW pWindow) { - - const auto PWINDOW = pWindow ? pWindow : Desktop::focusState()->window(); - - if (!validMapped(PWINDOW)) - return; - - const auto PNODE = getNodeFromWindow(PWINDOW); - - if (!PNODE) { - *PWINDOW->m_realSize = (PWINDOW->m_realSize->goal() + pixResize) - .clamp(PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}), - PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY})); - PWINDOW->updateWindowDecos(); - return; - } - - static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); - static auto PSMARTRESIZING = CConfigValue("dwindle:smart_resizing"); - - // get some data about our window - const auto PMONITOR = PWINDOW->m_monitor.lock(); - const auto MONITOR_WORKAREA = PMONITOR->logicalBoxMinusReserved(); - const bool DISPLAYLEFT = STICKS(PWINDOW->m_position.x, MONITOR_WORKAREA.x); - const bool DISPLAYRIGHT = STICKS(PWINDOW->m_position.x + PWINDOW->m_size.x, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); - const bool DISPLAYTOP = STICKS(PWINDOW->m_position.y, MONITOR_WORKAREA.y); - const bool DISPLAYBOTTOM = STICKS(PWINDOW->m_position.y + PWINDOW->m_size.y, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h); - - if (PWINDOW->m_isPseudotiled) { - if (!m_pseudoDragFlags.started) { - m_pseudoDragFlags.started = true; - - const auto pseudoSize = PWINDOW->m_realSize->goal(); - const auto mouseOffset = g_pInputManager->getMouseCoordsInternal() - (PNODE->box.pos() + ((PNODE->box.size() / 2) - (pseudoSize / 2))); - - if (mouseOffset.x > 0 && mouseOffset.x < pseudoSize.x && mouseOffset.y > 0 && mouseOffset.y < pseudoSize.y) { - m_pseudoDragFlags.pseudo = true; - m_pseudoDragFlags.xExtent = mouseOffset.x > pseudoSize.x / 2; - m_pseudoDragFlags.yExtent = mouseOffset.y > pseudoSize.y / 2; - - PWINDOW->m_pseudoSize = pseudoSize; - } else { - m_pseudoDragFlags.pseudo = false; - } - } - - if (m_pseudoDragFlags.pseudo) { - if (m_pseudoDragFlags.xExtent) - PWINDOW->m_pseudoSize.x += pixResize.x * 2; - else - PWINDOW->m_pseudoSize.x -= pixResize.x * 2; - if (m_pseudoDragFlags.yExtent) - PWINDOW->m_pseudoSize.y += pixResize.y * 2; - else - PWINDOW->m_pseudoSize.y -= pixResize.y * 2; - - CBox wbox = PNODE->box; - wbox.round(); - - Vector2D minSize = PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{30.0, 30.0}); - Vector2D maxSize = PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}); - Vector2D upperBound = Vector2D{std::min(maxSize.x, wbox.w), std::min(maxSize.y, wbox.h)}; - - PWINDOW->m_pseudoSize = PWINDOW->m_pseudoSize.clamp(minSize, upperBound); - - PWINDOW->m_lastFloatingSize = PWINDOW->m_pseudoSize; - PNODE->recalcSizePosRecursive(*PANIMATE == 0); - - return; - } - } - - // construct allowed movement - Vector2D allowedMovement = pixResize; - if (DISPLAYLEFT && DISPLAYRIGHT) - allowedMovement.x = 0; - - if (DISPLAYBOTTOM && DISPLAYTOP) - allowedMovement.y = 0; - - if (*PSMARTRESIZING == 1) { - // Identify inner and outer nodes for both directions - SP PVOUTER = nullptr; - SP PVINNER = nullptr; - SP PHOUTER = nullptr; - SP 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 NONE = corner == CORNER_NONE; - - for (auto PCURRENT = PNODE; PCURRENT && PCURRENT->pParent; PCURRENT = PCURRENT->pParent.lock()) { - const auto PPARENT = PCURRENT->pParent; - - if (!PVOUTER && PPARENT->splitTop && (NONE || (TOP && PPARENT->children[1] == PCURRENT) || (BOTTOM && PPARENT->children[0] == PCURRENT))) - PVOUTER = PCURRENT; - else if (!PVOUTER && !PVINNER && PPARENT->splitTop) - PVINNER = PCURRENT; - else if (!PHOUTER && !PPARENT->splitTop && (NONE || (LEFT && PPARENT->children[1] == PCURRENT) || (RIGHT && PPARENT->children[0] == PCURRENT))) - PHOUTER = PCURRENT; - else if (!PHOUTER && !PHINNER && !PPARENT->splitTop) - PHINNER = PCURRENT; - - if (PVOUTER && PHOUTER) - break; - } - - if (PHOUTER) { - PHOUTER->pParent->splitRatio = std::clamp(PHOUTER->pParent->splitRatio + allowedMovement.x * 2.f / PHOUTER->pParent->box.w, 0.1, 1.9); - - if (PHINNER) { - const auto ORIGINAL = PHINNER->box.w; - PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - if (PHINNER->pParent->children[0] == PHINNER) - PHINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9); - else - PHINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9); - PHINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - } else - PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - } - - if (PVOUTER) { - PVOUTER->pParent->splitRatio = std::clamp(PVOUTER->pParent->splitRatio + allowedMovement.y * 2.f / PVOUTER->pParent->box.h, 0.1, 1.9); - - if (PVINNER) { - const auto ORIGINAL = PVINNER->box.h; - PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - if (PVINNER->pParent->children[0] == PVINNER) - PVINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9); - else - PVINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9); - PVINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - } else - PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); - } - } else { - // get the correct containers to apply splitratio to - const auto PPARENT = PNODE->pParent; - - if (!PPARENT) - return; // the only window on a workspace, ignore - - const bool PARENTSIDEBYSIDE = !PPARENT->splitTop; - - // Get the parent's parent - auto PPARENT2 = PPARENT->pParent; - - // No parent means we have only 2 windows, and thus one axis of freedom - if (!PPARENT2) { - if (PARENTSIDEBYSIDE) { - allowedMovement.x *= 2.f / PPARENT->box.w; - PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9); - PPARENT->recalcSizePosRecursive(*PANIMATE == 0); - } else { - allowedMovement.y *= 2.f / PPARENT->box.h; - PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9); - PPARENT->recalcSizePosRecursive(*PANIMATE == 0); - } - - return; - } - - // Get first parent with other split - while (PPARENT2 && PPARENT2->splitTop == !PARENTSIDEBYSIDE) - PPARENT2 = PPARENT2->pParent; - - // no parent, one axis of freedom - if (!PPARENT2) { - if (PARENTSIDEBYSIDE) { - allowedMovement.x *= 2.f / PPARENT->box.w; - PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9); - PPARENT->recalcSizePosRecursive(*PANIMATE == 0); - } else { - allowedMovement.y *= 2.f / PPARENT->box.h; - PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9); - PPARENT->recalcSizePosRecursive(*PANIMATE == 0); - } - - return; - } - - // 2 axes of freedom - const auto SIDECONTAINER = PARENTSIDEBYSIDE ? PPARENT : PPARENT2; - const auto TOPCONTAINER = PARENTSIDEBYSIDE ? PPARENT2 : PPARENT; - - allowedMovement.x *= 2.f / SIDECONTAINER->box.w; - allowedMovement.y *= 2.f / TOPCONTAINER->box.h; - - SIDECONTAINER->splitRatio = std::clamp(SIDECONTAINER->splitRatio + allowedMovement.x, 0.1, 1.9); - TOPCONTAINER->splitRatio = std::clamp(TOPCONTAINER->splitRatio + allowedMovement.y, 0.1, 1.9); - SIDECONTAINER->recalcSizePosRecursive(*PANIMATE == 0); - TOPCONTAINER->recalcSizePosRecursive(*PANIMATE == 0); - } -} - -void CHyprDwindleLayout::fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) { - const auto PMONITOR = pWindow->m_monitor.lock(); - const auto PWORKSPACE = pWindow->m_workspace; - - // save position and size if floating - if (pWindow->m_isFloating && CURRENT_EFFECTIVE_MODE == FSMODE_NONE) { - pWindow->m_lastFloatingSize = pWindow->m_realSize->goal(); - pWindow->m_lastFloatingPosition = pWindow->m_realPosition->goal(); - pWindow->m_position = pWindow->m_realPosition->goal(); - pWindow->m_size = pWindow->m_realSize->goal(); - } - - if (EFFECTIVE_MODE == FSMODE_NONE) { - // if it got its fullscreen disabled, set back its node if it had one - const auto PNODE = getNodeFromWindow(pWindow); - if (PNODE) - applyNodeDataToWindow(PNODE); - else { - // get back its' dimensions from position and size - *pWindow->m_realPosition = pWindow->m_lastFloatingPosition; - *pWindow->m_realSize = pWindow->m_lastFloatingSize; - - pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - pWindow->updateWindowData(); - } - } else { - // apply new pos and size being monitors' box - if (EFFECTIVE_MODE == FSMODE_FULLSCREEN) { - *pWindow->m_realPosition = PMONITOR->m_position; - *pWindow->m_realSize = PMONITOR->m_size; - } else { - // This is a massive hack. - // We make a fake "only" node and apply - // To keep consistent with the settings without C+P code - - SP fakeNode = makeShared(); - fakeNode->self = fakeNode; - fakeNode->pWindow = pWindow; - fakeNode->box = PMONITOR->logicalBoxMinusReserved(); - fakeNode->workspaceID = pWindow->workspaceID(); - pWindow->m_position = fakeNode->box.pos(); - pWindow->m_size = fakeNode->box.size(); - fakeNode->ignoreFullscreenChecks = true; - fakeNode->layout = this; - - applyNodeDataToWindow(fakeNode); - } - } - - g_pCompositor->changeWindowZOrder(pWindow, true); -} - -void CHyprDwindleLayout::recalculateWindow(PHLWINDOW pWindow) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE) - return; - - PNODE->recalcSizePosRecursive(); -} - -SWindowRenderLayoutHints CHyprDwindleLayout::requestRenderHints(PHLWINDOW pWindow) { - // window should be valid, insallah - SWindowRenderLayoutHints hints; - - const auto PNODE = getNodeFromWindow(pWindow); - if (!PNODE) - return hints; // left for the future, maybe floating funkiness - - return hints; -} - -void CHyprDwindleLayout::moveWindowTo(PHLWINDOW pWindow, const std::string& dir, bool silent) { - if (!isDirection(dir)) - return; - - const auto PNODE = getNodeFromWindow(pWindow); - const auto originalWorkspaceID = pWindow->workspaceID(); - const Vector2D originalPos = pWindow->middle(); - - if (!PNODE || !pWindow->m_monitor) - return; - - Vector2D focalPoint; - - const auto WINDOWIDEALBB = pWindow->isFullscreen() ? CBox{pWindow->m_monitor->m_position, pWindow->m_monitor->m_size} : pWindow->getWindowIdealBoundingBoxIgnoreReserved(); - - switch (dir[0]) { - case 't': - case 'u': focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, -1.0}; break; - case 'd': - case 'b': focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, WINDOWIDEALBB.size().y + 1.0}; break; - case 'l': focalPoint = WINDOWIDEALBB.pos() + Vector2D{-1.0, WINDOWIDEALBB.size().y / 2.0}; break; - case 'r': focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x + 1.0, WINDOWIDEALBB.size().y / 2.0}; break; - default: UNREACHABLE(); - } - - pWindow->setAnimationsToMove(); - - onWindowRemovedTiling(pWindow); - - m_overrideFocalPoint = focalPoint; - - const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(focalPoint); - - if (PMONITORFOCAL != pWindow->m_monitor) { - pWindow->moveToWorkspace(PMONITORFOCAL->m_activeWorkspace); - pWindow->m_monitor = PMONITORFOCAL; - } - - pWindow->updateGroupOutputs(); - if (!pWindow->m_groupData.pNextWindow.expired()) { - PHLWINDOW next = pWindow->m_groupData.pNextWindow.lock(); - while (next != pWindow) { - next->updateToplevel(); - next = next->m_groupData.pNextWindow.lock(); - } - } - - onWindowCreatedTiling(pWindow); - - m_overrideFocalPoint.reset(); - - // restore focus to the previous position - if (silent) { - const auto PNODETOFOCUS = getClosestNodeOnWorkspace(originalWorkspaceID, originalPos); - if (PNODETOFOCUS && PNODETOFOCUS->pWindow.lock()) - Desktop::focusState()->fullWindowFocus(PNODETOFOCUS->pWindow.lock()); - } -} - -void CHyprDwindleLayout::switchWindows(PHLWINDOW pWindow, PHLWINDOW pWindow2) { - // windows should be valid, insallah - - auto PNODE = getNodeFromWindow(pWindow); - auto PNODE2 = getNodeFromWindow(pWindow2); - - if (!PNODE2 || !PNODE) - return; - - const eFullscreenMode MODE1 = pWindow->m_fullscreenState.internal; - const eFullscreenMode MODE2 = pWindow2->m_fullscreenState.internal; - - g_pCompositor->setWindowFullscreenInternal(pWindow, FSMODE_NONE); - g_pCompositor->setWindowFullscreenInternal(pWindow2, FSMODE_NONE); - - SDwindleNodeData* ACTIVE1 = nullptr; - SDwindleNodeData* ACTIVE2 = nullptr; - - // swap the windows and recalc - PNODE2->pWindow = pWindow; - PNODE->pWindow = pWindow2; - - if (PNODE->workspaceID != PNODE2->workspaceID) { - std::swap(pWindow2->m_monitor, pWindow->m_monitor); - std::swap(pWindow2->m_workspace, pWindow->m_workspace); - } - - pWindow->setAnimationsToMove(); - pWindow2->setAnimationsToMove(); - - // recalc the workspace - getMasterNodeOnWorkspace(PNODE->workspaceID)->recalcSizePosRecursive(); - - if (PNODE2->workspaceID != PNODE->workspaceID) - getMasterNodeOnWorkspace(PNODE2->workspaceID)->recalcSizePosRecursive(); - - if (ACTIVE1) { - ACTIVE1->box = PNODE->box; - ACTIVE1->pWindow->m_position = ACTIVE1->box.pos(); - ACTIVE1->pWindow->m_size = ACTIVE1->box.size(); - } - - if (ACTIVE2) { - ACTIVE2->box = PNODE2->box; - ACTIVE2->pWindow->m_position = ACTIVE2->box.pos(); - ACTIVE2->pWindow->m_size = ACTIVE2->box.size(); - } - - g_pHyprRenderer->damageWindow(pWindow); - g_pHyprRenderer->damageWindow(pWindow2); - - g_pCompositor->setWindowFullscreenInternal(pWindow2, MODE1); - g_pCompositor->setWindowFullscreenInternal(pWindow, MODE2); -} - -void CHyprDwindleLayout::alterSplitRatio(PHLWINDOW pWindow, float ratio, bool exact) { - // window should be valid, insallah - - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE || !PNODE->pParent) - return; - - float newRatio = exact ? ratio : PNODE->pParent->splitRatio + ratio; - PNODE->pParent->splitRatio = std::clamp(newRatio, 0.1f, 1.9f); - - PNODE->pParent->recalcSizePosRecursive(); -} - -std::any CHyprDwindleLayout::layoutMessage(SLayoutMessageHeader header, std::string message) { - const auto ARGS = CVarList(message, 0, ' '); - if (ARGS[0] == "togglesplit") { - toggleSplit(header.pWindow); - } else if (ARGS[0] == "swapsplit") { - swapSplit(header.pWindow); - } else if (ARGS[0] == "movetoroot") { - const auto WINDOW = ARGS[1].empty() ? header.pWindow : g_pCompositor->getWindowByRegex(ARGS[1]); - const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable"; - moveToRoot(WINDOW, STABLE); - } else if (ARGS[0] == "preselect") { - std::string direction = ARGS[1]; - - if (direction.empty()) { - Log::logger->log(Log::ERR, "Expected direction for preselect"); - return ""; - } - - switch (direction.front()) { - case 'u': - case 't': { - m_overrideDirection = DIRECTION_UP; - break; - } - case 'd': - case 'b': { - m_overrideDirection = DIRECTION_DOWN; - break; - } - case 'r': { - m_overrideDirection = DIRECTION_RIGHT; - break; - } - case 'l': { - m_overrideDirection = DIRECTION_LEFT; - break; - } - default: { - // any other character resets the focus direction - // needed for the persistent mode - m_overrideDirection = DIRECTION_DEFAULT; - break; - } - } - } - - return ""; -} - -void CHyprDwindleLayout::toggleSplit(PHLWINDOW pWindow) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE || !PNODE->pParent) - return; - - if (pWindow->isFullscreen()) - return; - - PNODE->pParent->splitTop = !PNODE->pParent->splitTop; - - PNODE->pParent->recalcSizePosRecursive(); -} - -void CHyprDwindleLayout::swapSplit(PHLWINDOW pWindow) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE || !PNODE->pParent) - return; - - if (pWindow->isFullscreen()) - return; - - std::swap(PNODE->pParent->children[0], PNODE->pParent->children[1]); - - PNODE->pParent->recalcSizePosRecursive(); -} - -// goal: maximize the chosen window within current dwindle layout -// impl: swap the selected window with the other sub-tree below root -void CHyprDwindleLayout::moveToRoot(PHLWINDOW pWindow, bool stable) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE || !PNODE->pParent) - return; - - if (pWindow->isFullscreen()) - return; - - // already at root - if (!PNODE->pParent->pParent) - return; - - auto& pNode = PNODE->pParent->children[0] == PNODE ? PNODE->pParent->children[0] : PNODE->pParent->children[1]; - - // instead of [getMasterNodeOnWorkspace], we walk back to root since we need - // to know which children of root is our ancestor - auto pAncestor = PNODE, pRoot = PNODE->pParent.lock(); - while (pRoot->pParent) { - pAncestor = pRoot; - pRoot = pRoot->pParent.lock(); - } - - auto& pSwap = pRoot->children[0] == pAncestor ? pRoot->children[1] : pRoot->children[0]; - std::swap(pNode, pSwap); - std::swap(pNode->pParent, pSwap->pParent); - - // [stable] in that the focused window occupies same side of screen - if (stable) - std::swap(pRoot->children[0], pRoot->children[1]); - - // if the workspace is visible, recalculate layout - if (pWindow->m_workspace && pWindow->m_workspace->isVisible()) - pRoot->recalcSizePosRecursive(); -} - -void CHyprDwindleLayout::replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) { - const auto PNODE = getNodeFromWindow(from); - - if (!PNODE) - return; - - PNODE->pWindow = to; - - applyNodeDataToWindow(PNODE, true); -} - -std::string CHyprDwindleLayout::getLayoutName() { - return "dwindle"; -} - -void CHyprDwindleLayout::onEnable() { - for (auto const& w : g_pCompositor->m_windows) { - if (w->m_isFloating || !w->m_isMapped || w->isHidden()) - continue; - - onWindowCreatedTiling(w); - } -} - -void CHyprDwindleLayout::onDisable() { - m_dwindleNodesData.clear(); -} - -Vector2D CHyprDwindleLayout::predictSizeForNewWindowTiled() { - if (!Desktop::focusState()->monitor()) - return {}; - - // get window candidate - PHLWINDOW candidate = Desktop::focusState()->window(); - - if (!candidate) - candidate = Desktop::focusState()->monitor()->m_activeWorkspace->getFirstWindow(); - - // create a fake node - SDwindleNodeData node; - - if (!candidate) - return Desktop::focusState()->monitor()->m_size; - else { - const auto PNODE = getNodeFromWindow(candidate); - - if (!PNODE) - return {}; - - node = *PNODE; - node.pWindow.reset(); - - CBox box = PNODE->box; - - static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); - - bool splitTop = box.h * *PFLMULT > box.w; - - const auto SPLITSIDE = !splitTop; - - if (SPLITSIDE) - node.box = {{}, {box.w / 2.0, box.h}}; - else - node.box = {{}, {box.w, box.h / 2.0}}; - - // TODO: make this better and more accurate - - return node.box.size(); - } - - return {}; -} diff --git a/src/layout/DwindleLayout.hpp b/src/layout/DwindleLayout.hpp deleted file mode 100644 index e704b8f4..00000000 --- a/src/layout/DwindleLayout.hpp +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#include "IHyprLayout.hpp" -#include "../desktop/DesktopTypes.hpp" - -#include -#include -#include -#include -#include - -class CHyprDwindleLayout; -enum eFullscreenMode : int8_t; - -struct SDwindleNodeData { - WP pParent; - bool isNode = false; - - PHLWINDOWREF pWindow; - - std::array, 2> children = {}; - WP self; - - bool splitTop = false; // for preserve_split - - CBox box = {0}; - - WORKSPACEID workspaceID = WORKSPACE_INVALID; - - float splitRatio = 1.f; - - bool valid = true; - - bool ignoreFullscreenChecks = false; - - // For list lookup - bool operator==(const SDwindleNodeData& rhs) const { - return pWindow.lock() == rhs.pWindow.lock() && workspaceID == rhs.workspaceID && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] && - children[1] == rhs.children[1]; - } - - void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false); - void applyRootBox(); - CHyprDwindleLayout* layout = nullptr; -}; - -class CHyprDwindleLayout : public IHyprLayout { - public: - virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT); - virtual void onWindowRemovedTiling(PHLWINDOW); - virtual bool isWindowTiled(PHLWINDOW); - virtual void recalculateMonitor(const MONITORID&); - virtual void recalculateWindow(PHLWINDOW); - virtual void onBeginDragWindow(); - virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr); - virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE); - virtual std::any layoutMessage(SLayoutMessageHeader, std::string); - virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW); - virtual void switchWindows(PHLWINDOW, PHLWINDOW); - virtual void moveWindowTo(PHLWINDOW, const std::string& dir, bool silent); - virtual void alterSplitRatio(PHLWINDOW, float, bool); - virtual std::string getLayoutName(); - virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to); - virtual Vector2D predictSizeForNewWindowTiled(); - - virtual void onEnable(); - virtual void onDisable(); - - private: - std::vector> m_dwindleNodesData; - - struct { - bool started = false; - bool pseudo = false; - bool xExtent = false; - bool yExtent = false; - } m_pseudoDragFlags; - - std::optional m_overrideFocalPoint; // for onWindowCreatedTiling. - - int getNodesOnWorkspace(const WORKSPACEID&); - void applyNodeDataToWindow(SP, bool force = false); - void calculateWorkspace(const PHLWORKSPACE& pWorkspace); - SP getNodeFromWindow(PHLWINDOW); - SP getFirstNodeOnWorkspace(const WORKSPACEID&); - SP getClosestNodeOnWorkspace(const WORKSPACEID&, const Vector2D&); - SP getMasterNodeOnWorkspace(const WORKSPACEID&); - - void toggleSplit(PHLWINDOW); - void swapSplit(PHLWINDOW); - void moveToRoot(PHLWINDOW, bool stable = true); - - eDirection m_overrideDirection = DIRECTION_DEFAULT; - - friend struct SDwindleNodeData; -}; - -template -struct std::formatter, CharT> : std::formatter { - template - auto format(const SP& node, FormatContext& ctx) const { - auto out = ctx.out(); - if (!node) - return std::format_to(out, "[Node nullptr]"); - std::format_to(out, "[Node {:x}: workspace: {}, pos: {:j2}, size: {:j2}", rc(node.get()), node->workspaceID, node->box.pos(), node->box.size()); - if (!node->isNode && !node->pWindow.expired()) - std::format_to(out, ", window: {:x}", node->pWindow.lock()); - return std::format_to(out, "]"); - } -}; diff --git a/src/layout/IHyprLayout.cpp b/src/layout/IHyprLayout.cpp deleted file mode 100644 index f434b580..00000000 --- a/src/layout/IHyprLayout.cpp +++ /dev/null @@ -1,1062 +0,0 @@ -#include "IHyprLayout.hpp" -#include "../defines.hpp" -#include "../Compositor.hpp" -#include "../render/decorations/CHyprGroupBarDecoration.hpp" -#include "../config/ConfigValue.hpp" -#include "../config/ConfigManager.hpp" -#include "../desktop/view/Window.hpp" -#include "../desktop/state/FocusState.hpp" -#include "../protocols/XDGShell.hpp" -#include "../protocols/core/Compositor.hpp" -#include "../xwayland/XSurface.hpp" -#include "../render/Renderer.hpp" -#include "../managers/LayoutManager.hpp" -#include "../managers/EventManager.hpp" -#include "../managers/HookSystemManager.hpp" -#include "../managers/cursor/CursorShapeOverrideController.hpp" -#include "../desktop/rule/windowRule/WindowRule.hpp" - -void IHyprLayout::onWindowCreated(PHLWINDOW pWindow, eDirection direction) { - CBox desiredGeometry = g_pXWaylandManager->getGeometryForWindow(pWindow); - - const bool HASPERSISTENTSIZE = pWindow->m_ruleApplicator->persistentSize().valueOrDefault(); - - const auto STOREDSIZE = HASPERSISTENTSIZE ? g_pConfigManager->getStoredFloatingSize(pWindow) : std::nullopt; - - if (STOREDSIZE.has_value()) { - Log::logger->log(Log::DEBUG, "using stored size {}x{} for new window {}::{}", STOREDSIZE->x, STOREDSIZE->y, pWindow->m_class, pWindow->m_title); - pWindow->m_lastFloatingSize = STOREDSIZE.value(); - } else if (desiredGeometry.width <= 5 || desiredGeometry.height <= 5) { - const auto PMONITOR = pWindow->m_monitor.lock(); - pWindow->m_lastFloatingSize = PMONITOR->m_size / 2.f; - } else - pWindow->m_lastFloatingSize = Vector2D(desiredGeometry.width, desiredGeometry.height); - - pWindow->m_pseudoSize = pWindow->m_lastFloatingSize; - - bool autoGrouped = IHyprLayout::onWindowCreatedAutoGroup(pWindow); - if (autoGrouped) - return; - - if (pWindow->m_isFloating) - onWindowCreatedFloating(pWindow); - else - onWindowCreatedTiling(pWindow, direction); - - if (!g_pXWaylandManager->shouldBeFloated(pWindow)) // do not apply group rules to child windows - pWindow->applyGroupRules(); -} - -void IHyprLayout::onWindowRemoved(PHLWINDOW pWindow) { - if (pWindow->isFullscreen()) - g_pCompositor->setWindowFullscreenInternal(pWindow, FSMODE_NONE); - - if (!pWindow->m_groupData.pNextWindow.expired()) { - if (pWindow->m_groupData.pNextWindow.lock() == pWindow) { - pWindow->m_groupData.pNextWindow.reset(); - pWindow->updateWindowDecos(); - } else { - // find last window and update - PHLWINDOW PWINDOWPREV = pWindow->getGroupPrevious(); - const auto WINDOWISVISIBLE = pWindow->getGroupCurrent() == pWindow; - - if (WINDOWISVISIBLE) - PWINDOWPREV->setGroupCurrent(pWindow->m_groupData.head ? pWindow->m_groupData.pNextWindow.lock() : PWINDOWPREV); - - PWINDOWPREV->m_groupData.pNextWindow = pWindow->m_groupData.pNextWindow; - - pWindow->m_groupData.pNextWindow.reset(); - - if (pWindow->m_groupData.head) { - std::swap(PWINDOWPREV->m_groupData.pNextWindow->m_groupData.head, pWindow->m_groupData.head); - std::swap(PWINDOWPREV->m_groupData.pNextWindow->m_groupData.locked, pWindow->m_groupData.locked); - } - - if (pWindow == m_lastTiledWindow) - m_lastTiledWindow.reset(); - - pWindow->setHidden(false); - - pWindow->updateWindowDecos(); - PWINDOWPREV->getGroupCurrent()->updateWindowDecos(); - pWindow->updateDecorationValues(); - - return; - } - } - - if (pWindow->m_isFloating) { - onWindowRemovedFloating(pWindow); - } else { - onWindowRemovedTiling(pWindow); - } - - if (pWindow == m_lastTiledWindow) - m_lastTiledWindow.reset(); -} - -void IHyprLayout::onWindowRemovedFloating(PHLWINDOW pWindow) { - ; // no-op -} - -void IHyprLayout::onWindowCreatedFloating(PHLWINDOW pWindow) { - - CBox desiredGeometry = g_pXWaylandManager->getGeometryForWindow(pWindow); - const auto PMONITOR = pWindow->m_monitor.lock(); - - if (pWindow->m_isX11) { - Vector2D xy = {desiredGeometry.x, desiredGeometry.y}; - xy = g_pXWaylandManager->xwaylandToWaylandCoords(xy); - desiredGeometry.x = xy.x; - desiredGeometry.y = xy.y; - } else if (pWindow->m_ruleApplicator->persistentSize().valueOrDefault()) { - desiredGeometry.w = pWindow->m_lastFloatingSize.x; - desiredGeometry.h = pWindow->m_lastFloatingSize.y; - } - - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); - - if (!PMONITOR) { - Log::logger->log(Log::ERR, "{:m} has an invalid monitor in onWindowCreatedFloating!!!", pWindow); - return; - } - - if (desiredGeometry.width <= 5 || desiredGeometry.height <= 5) { - const auto PWINDOWSURFACE = pWindow->wlSurface()->resource(); - *pWindow->m_realSize = PWINDOWSURFACE->m_current.size; - - if ((desiredGeometry.width <= 1 || desiredGeometry.height <= 1) && pWindow->m_isX11 && - pWindow->isX11OverrideRedirect()) { // XDG windows should be fine. TODO: check for weird atoms? - pWindow->setHidden(true); - return; - } - - // reject any windows with size <= 5x5 - if (pWindow->m_realSize->goal().x <= 5 || pWindow->m_realSize->goal().y <= 5) - *pWindow->m_realSize = PMONITOR->m_size / 2.f; - - if (pWindow->m_isX11 && pWindow->isX11OverrideRedirect()) { - - if (pWindow->m_xwaylandSurface->m_geometry.x != 0 && pWindow->m_xwaylandSurface->m_geometry.y != 0) - *pWindow->m_realPosition = g_pXWaylandManager->xwaylandToWaylandCoords(pWindow->m_xwaylandSurface->m_geometry.pos()); - else - *pWindow->m_realPosition = Vector2D(PMONITOR->m_position.x + (PMONITOR->m_size.x - pWindow->m_realSize->goal().x) / 2.f, - PMONITOR->m_position.y + (PMONITOR->m_size.y - pWindow->m_realSize->goal().y) / 2.f); - } else { - *pWindow->m_realPosition = Vector2D(PMONITOR->m_position.x + (PMONITOR->m_size.x - pWindow->m_realSize->goal().x) / 2.f, - PMONITOR->m_position.y + (PMONITOR->m_size.y - pWindow->m_realSize->goal().y) / 2.f); - } - } else { - // we respect the size. - *pWindow->m_realSize = Vector2D(desiredGeometry.width, desiredGeometry.height); - - // check if it's on the correct monitor! - Vector2D middlePoint = Vector2D(desiredGeometry.x, desiredGeometry.y) + Vector2D(desiredGeometry.width, desiredGeometry.height) / 2.f; - - // check if it's visible on any monitor (only for XDG) - bool visible = pWindow->m_isX11; - - if (!visible) { - visible = g_pCompositor->isPointOnAnyMonitor(Vector2D(desiredGeometry.x, desiredGeometry.y)) && - g_pCompositor->isPointOnAnyMonitor(Vector2D(desiredGeometry.x + desiredGeometry.width, desiredGeometry.y)) && - g_pCompositor->isPointOnAnyMonitor(Vector2D(desiredGeometry.x, desiredGeometry.y + desiredGeometry.height)) && - g_pCompositor->isPointOnAnyMonitor(Vector2D(desiredGeometry.x + desiredGeometry.width, desiredGeometry.y + desiredGeometry.height)); - } - - // TODO: detect a popup in a more consistent way. - bool centeredOnParent = false; - if ((desiredGeometry.x == 0 && desiredGeometry.y == 0) || !visible || !pWindow->m_isX11) { - // if the pos isn't set, fall back to the center placement if it's not a child - auto pos = PMONITOR->m_position + PMONITOR->m_size / 2.F - desiredGeometry.size() / 2.F; - - // otherwise middle of parent if available - if (!pWindow->m_isX11) { - if (const auto PARENT = pWindow->parent(); PARENT) { - *pWindow->m_realPosition = PARENT->m_realPosition->goal() + PARENT->m_realSize->goal() / 2.F - desiredGeometry.size() / 2.F; - pWindow->m_workspace = PARENT->m_workspace; - pWindow->m_monitor = PARENT->m_monitor; - centeredOnParent = true; - } - } - if (!centeredOnParent) - *pWindow->m_realPosition = pos; - } else { - // if it is, we respect where it wants to put itself, but apply monitor offset if outside - // most of these are popups - - if (const auto POPENMON = g_pCompositor->getMonitorFromVector(middlePoint); POPENMON->m_id != PMONITOR->m_id) - *pWindow->m_realPosition = Vector2D(desiredGeometry.x, desiredGeometry.y) - POPENMON->m_position + PMONITOR->m_position; - else - *pWindow->m_realPosition = Vector2D(desiredGeometry.x, desiredGeometry.y); - } - } - - if (*PXWLFORCESCALEZERO && pWindow->m_isX11) - *pWindow->m_realSize = pWindow->m_realSize->goal() / PMONITOR->m_scale; - - if (pWindow->m_X11DoesntWantBorders || (pWindow->m_isX11 && pWindow->isX11OverrideRedirect())) { - pWindow->m_realPosition->warp(); - pWindow->m_realSize->warp(); - } - - if (!pWindow->isX11OverrideRedirect()) - g_pCompositor->changeWindowZOrder(pWindow, true); - else { - pWindow->m_pendingReportedSize = pWindow->m_realSize->goal(); - pWindow->m_reportedSize = pWindow->m_pendingReportedSize; - } -} - -bool IHyprLayout::onWindowCreatedAutoGroup(PHLWINDOW pWindow) { - static auto PAUTOGROUP = CConfigValue("group:auto_group"); - const PHLWINDOW OPENINGON = Desktop::focusState()->window() && Desktop::focusState()->window()->m_workspace == pWindow->m_workspace ? - Desktop::focusState()->window() : - (pWindow->m_workspace ? pWindow->m_workspace->getFirstWindow() : nullptr); - const bool FLOATEDINTOTILED = pWindow->m_isFloating && OPENINGON && !OPENINGON->m_isFloating; - const bool SWALLOWING = pWindow->m_swallowed || pWindow->m_groupSwallowed; - - if ((*PAUTOGROUP || SWALLOWING) // continue if auto_group is enabled or if dealing with window swallowing. - && OPENINGON // this shouldn't be 0, but honestly, better safe than sorry. - && OPENINGON != pWindow // prevent freeze when the "group set" window rule makes the new window to be already a group. - && OPENINGON->m_groupData.pNextWindow.lock() // check if OPENINGON is a group. - && pWindow->canBeGroupedInto(OPENINGON) // check if the new window can be grouped into OPENINGON. - && !g_pXWaylandManager->shouldBeFloated(pWindow) // don't group child windows. Fix for floated groups. Tiled groups don't need this because we check if !FLOATEDINTOTILED. - && !FLOATEDINTOTILED) { // don't group a new floated window into a tiled group (for convenience). - - pWindow->m_isFloating = OPENINGON->m_isFloating; // match the floating state. Needed to autogroup a new tiled window into a floated group. - - static auto USECURRPOS = CConfigValue("group:insert_after_current"); - (*USECURRPOS ? OPENINGON : OPENINGON->getGroupTail())->insertWindowToGroup(pWindow); - - OPENINGON->setGroupCurrent(pWindow); - pWindow->applyGroupRules(); - pWindow->updateWindowDecos(); - recalculateWindow(pWindow); - - return true; - } - - return false; -} - -void IHyprLayout::onBeginDragWindow() { - const auto DRAGGINGWINDOW = g_pInputManager->m_currentlyDraggedWindow.lock(); - static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); - - m_mouseMoveEventCount = 1; - m_beginDragSizeXY = Vector2D(); - - // Window will be floating. Let's check if it's valid. It should be, but I don't like crashing. - if (!validMapped(DRAGGINGWINDOW)) { - Log::logger->log(Log::ERR, "Dragging attempted on an invalid window!"); - CKeybindManager::changeMouseBindMode(MBIND_INVALID); - return; - } - - // Try to pick up dragged window now if drag_threshold is disabled - // or at least update dragging related variables for the cursors - g_pInputManager->m_dragThresholdReached = *PDRAGTHRESHOLD <= 0; - if (updateDragWindow()) - return; - - // get the grab corner - static auto RESIZECORNER = CConfigValue("general:resize_corner"); - if (*RESIZECORNER != 0 && *RESIZECORNER <= 4 && DRAGGINGWINDOW->m_isFloating) { - switch (*RESIZECORNER) { - case 1: - m_grabbedCorner = CORNER_TOPLEFT; - Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - break; - case 2: - m_grabbedCorner = CORNER_TOPRIGHT; - Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - break; - case 3: - m_grabbedCorner = CORNER_BOTTOMRIGHT; - Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - break; - case 4: - m_grabbedCorner = CORNER_BOTTOMLEFT; - Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - break; - } - } else if (m_beginDragXY.x < m_beginDragPositionXY.x + m_beginDragSizeXY.x / 2.0) { - if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.0) { - m_grabbedCorner = CORNER_TOPLEFT; - Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - } else { - m_grabbedCorner = CORNER_BOTTOMLEFT; - Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - } - } else { - if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.0) { - m_grabbedCorner = CORNER_TOPRIGHT; - Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - } else { - m_grabbedCorner = CORNER_BOTTOMRIGHT; - Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - } - } - - if (g_pInputManager->m_dragMode != MBIND_RESIZE && g_pInputManager->m_dragMode != MBIND_RESIZE_FORCE_RATIO && g_pInputManager->m_dragMode != MBIND_RESIZE_BLOCK_RATIO) - Cursor::overrideController->setOverride("grabbing", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - - g_pHyprRenderer->damageWindow(DRAGGINGWINDOW); - - g_pKeybindManager->shadowKeybinds(); - - Desktop::focusState()->rawWindowFocus(DRAGGINGWINDOW); - g_pCompositor->changeWindowZOrder(DRAGGINGWINDOW, true); -} - -void IHyprLayout::onEndDragWindow() { - const auto DRAGGINGWINDOW = g_pInputManager->m_currentlyDraggedWindow.lock(); - - m_mouseMoveEventCount = 1; - - if (!validMapped(DRAGGINGWINDOW)) { - if (DRAGGINGWINDOW) { - Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - g_pInputManager->m_currentlyDraggedWindow.reset(); - } - return; - } - - Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); - g_pInputManager->m_currentlyDraggedWindow.reset(); - g_pInputManager->m_wasDraggingWindow = true; - - if (g_pInputManager->m_dragMode == MBIND_MOVE) { - g_pHyprRenderer->damageWindow(DRAGGINGWINDOW); - const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); - PHLWINDOW pWindow = - g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGINGWINDOW); - - if (pWindow) { - if (pWindow->checkInputOnDecos(INPUT_TYPE_DRAG_END, MOUSECOORDS, DRAGGINGWINDOW)) - return; - - const bool FLOATEDINTOTILED = !pWindow->m_isFloating && !DRAGGINGWINDOW->m_draggingTiled; - static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); - - if (pWindow->m_groupData.pNextWindow.lock() && DRAGGINGWINDOW->canBeGroupedInto(pWindow) && *PDRAGINTOGROUP == 1 && !FLOATEDINTOTILED) { - - if (DRAGGINGWINDOW->m_groupData.pNextWindow) { - PHLWINDOW next = DRAGGINGWINDOW->m_groupData.pNextWindow.lock(); - while (next != DRAGGINGWINDOW) { - next->m_isFloating = pWindow->m_isFloating; // match the floating state of group members - *next->m_realSize = pWindow->m_realSize->goal(); // match the size of group members - *next->m_realPosition = pWindow->m_realPosition->goal(); // match the position of group members - next = next->m_groupData.pNextWindow.lock(); - } - } - - DRAGGINGWINDOW->m_isFloating = pWindow->m_isFloating; // match the floating state of the window - DRAGGINGWINDOW->m_lastFloatingSize = m_draggingWindowOriginalFloatSize; - DRAGGINGWINDOW->m_draggingTiled = false; - - static auto USECURRPOS = CConfigValue("group:insert_after_current"); - (*USECURRPOS ? pWindow : pWindow->getGroupTail())->insertWindowToGroup(DRAGGINGWINDOW); - pWindow->setGroupCurrent(DRAGGINGWINDOW); - DRAGGINGWINDOW->applyGroupRules(); - DRAGGINGWINDOW->updateWindowDecos(); - } - } - } - - if (DRAGGINGWINDOW->m_draggingTiled) { - static auto PPRECISEMOUSE = CConfigValue("dwindle:precise_mouse_move"); - DRAGGINGWINDOW->m_isFloating = false; - g_pInputManager->refocus(); - - if (*PPRECISEMOUSE) { - eDirection direction = DIRECTION_DEFAULT; - - const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); - const PHLWINDOW pReferenceWindow = - g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGINGWINDOW); - - if (pReferenceWindow && pReferenceWindow != DRAGGINGWINDOW) { - const Vector2D draggedCenter = DRAGGINGWINDOW->m_realPosition->goal() + DRAGGINGWINDOW->m_realSize->goal() / 2.f; - const Vector2D referenceCenter = pReferenceWindow->m_realPosition->goal() + pReferenceWindow->m_realSize->goal() / 2.f; - const float xDiff = draggedCenter.x - referenceCenter.x; - const float yDiff = draggedCenter.y - referenceCenter.y; - - if (fabs(xDiff) > fabs(yDiff)) - direction = xDiff < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - else - direction = yDiff < 0 ? DIRECTION_UP : DIRECTION_DOWN; - } - - onWindowRemovedTiling(DRAGGINGWINDOW); - onWindowCreatedTiling(DRAGGINGWINDOW, direction); - } else - changeWindowFloatingMode(DRAGGINGWINDOW); - - DRAGGINGWINDOW->m_lastFloatingSize = m_draggingWindowOriginalFloatSize; - } - - g_pHyprRenderer->damageWindow(DRAGGINGWINDOW); - Desktop::focusState()->fullWindowFocus(DRAGGINGWINDOW); - - g_pInputManager->m_wasDraggingWindow = false; -} - -static inline bool canSnap(const double SIDEA, const double SIDEB, const double GAP) { - return std::abs(SIDEA - SIDEB) < GAP; -} - -static void snapMove(double& start, double& end, const double P) { - end = P + (end - start); - start = P; -} - -static void snapResize(double& start, double& end, const double P) { - start = P; -} - -using SnapFn = std::function; - -void IHyprLayout::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, PHLWINDOW DRAGGINGWINDOW, const eMouseBindMode MODE, const int CORNER, const Vector2D& BEGINSIZE) { - static auto SNAPWINDOWGAP = CConfigValue("general:snap:window_gap"); - static auto SNAPMONITORGAP = CConfigValue("general:snap:monitor_gap"); - static auto SNAPBORDEROVERLAP = CConfigValue("general:snap:border_overlap"); - static auto SNAPRESPECTGAPS = CConfigValue("general:snap:respect_gaps"); - - static auto PGAPSIN = CConfigValue("general:gaps_in"); - static auto PGAPSOUT = CConfigValue("general:gaps_out"); - const auto GAPSNONE = CCssGapData{0, 0, 0, 0}; - - const SnapFn SNAP = (MODE == MBIND_MOVE) ? snapMove : snapResize; - int snaps = 0; - - struct SRange { - double start = 0; - double end = 0; - }; - const auto EXTENTS = DRAGGINGWINDOW->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); - SRange sourceX = {sourcePos.x - EXTENTS.topLeft.x, sourcePos.x + sourceSize.x + EXTENTS.bottomRight.x}; - SRange sourceY = {sourcePos.y - EXTENTS.topLeft.y, sourcePos.y + sourceSize.y + EXTENTS.bottomRight.y}; - - if (*SNAPWINDOWGAP) { - const double GAPSIZE = *SNAPWINDOWGAP; - const auto WSID = DRAGGINGWINDOW->workspaceID(); - const bool HASFULLSCREEN = DRAGGINGWINDOW->m_workspace && DRAGGINGWINDOW->m_workspace->m_hasFullscreenWindow; - - const auto* GAPSIN = *SNAPRESPECTGAPS ? sc(PGAPSIN.ptr()->getData()) : &GAPSNONE; - const double GAPSX = GAPSIN->m_left + GAPSIN->m_right; - const double GAPSY = GAPSIN->m_top + GAPSIN->m_bottom; - - for (auto& other : g_pCompositor->m_windows) { - if ((HASFULLSCREEN && !other->m_createdOverFullscreen) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut || - other->isX11OverrideRedirect()) - continue; - - const CBox SURF = other->getWindowBoxUnified(Desktop::View::RESERVED_EXTENTS); - const SRange SURFBX = {SURF.x - GAPSX, SURF.x + SURF.w + GAPSX}; - const SRange SURFBY = {SURF.y - GAPSY, SURF.y + SURF.h + GAPSY}; - - // only snap windows if their ranges overlap in the opposite axis - if (sourceY.start <= SURFBY.end && SURFBY.start <= sourceY.end) { - if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && canSnap(sourceX.start, SURFBX.end, GAPSIZE)) { - SNAP(sourceX.start, sourceX.end, SURFBX.end); - snaps |= SNAP_LEFT; - } else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && canSnap(sourceX.end, SURFBX.start, GAPSIZE)) { - SNAP(sourceX.end, sourceX.start, SURFBX.start); - snaps |= SNAP_RIGHT; - } - } - if (sourceX.start <= SURFBX.end && SURFBX.start <= sourceX.end) { - if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && canSnap(sourceY.start, SURFBY.end, GAPSIZE)) { - SNAP(sourceY.start, sourceY.end, SURFBY.end); - snaps |= SNAP_UP; - } else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && canSnap(sourceY.end, SURFBY.start, GAPSIZE)) { - SNAP(sourceY.end, sourceY.start, SURFBY.start); - snaps |= SNAP_DOWN; - } - } - - // corner snapping - if (sourceX.start == SURFBX.end || SURFBX.start == sourceX.end) { - const SRange SURFY = {SURFBY.start + GAPSY, SURFBY.end - GAPSY}; - if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && !(snaps & SNAP_UP) && canSnap(sourceY.start, SURFY.start, GAPSIZE)) { - SNAP(sourceY.start, sourceY.end, SURFY.start); - snaps |= SNAP_UP; - } else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_DOWN) && canSnap(sourceY.end, SURFY.end, GAPSIZE)) { - SNAP(sourceY.end, sourceY.start, SURFY.end); - snaps |= SNAP_DOWN; - } - } - if (sourceY.start == SURFBY.end || SURFBY.start == sourceY.end) { - const SRange SURFX = {SURFBX.start + GAPSX, SURFBX.end - GAPSX}; - if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && !(snaps & SNAP_LEFT) && canSnap(sourceX.start, SURFX.start, GAPSIZE)) { - SNAP(sourceX.start, sourceX.end, SURFX.start); - snaps |= SNAP_LEFT; - } else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_RIGHT) && canSnap(sourceX.end, SURFX.end, GAPSIZE)) { - SNAP(sourceX.end, sourceX.start, SURFX.end); - snaps |= SNAP_RIGHT; - } - } - } - } - - if (*SNAPMONITORGAP) { - const double GAPSIZE = *SNAPMONITORGAP; - const auto EXTENTNONE = SBoxExtents{{0, 0}, {0, 0}}; - const auto* EXTENTDIFF = *SNAPBORDEROVERLAP ? &EXTENTS : &EXTENTNONE; - const auto MON = DRAGGINGWINDOW->m_monitor.lock(); - - const auto* GAPSOUT = *SNAPRESPECTGAPS ? sc(PGAPSOUT.ptr()->getData()) : &GAPSNONE; - const auto WORK_AREA = Desktop::CReservedArea{GAPSOUT->m_top, GAPSOUT->m_right, GAPSOUT->m_bottom, GAPSOUT->m_left}.apply(MON->logicalBoxMinusReserved()); - - SRange monX = {WORK_AREA.x, WORK_AREA.x + WORK_AREA.w}; - SRange monY = {WORK_AREA.y, WORK_AREA.y + WORK_AREA.h}; - - const bool HAS_LEFT = MON->m_reservedArea.left() > 0; - const bool HAS_TOP = MON->m_reservedArea.top() > 0; - const bool HAS_BOTTOM = MON->m_reservedArea.bottom() > 0; - const bool HAS_RIGHT = MON->m_reservedArea.right() > 0; - - if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && - ((HAS_LEFT && canSnap(sourceX.start, monX.start, GAPSIZE)) || canSnap(sourceX.start, (monX.start -= MON->m_reservedArea.left() + EXTENTDIFF->topLeft.x), GAPSIZE))) { - SNAP(sourceX.start, sourceX.end, monX.start); - snaps |= SNAP_LEFT; - } - if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && - ((HAS_RIGHT && canSnap(sourceX.end, monX.end, GAPSIZE)) || canSnap(sourceX.end, (monX.end += MON->m_reservedArea.right() + EXTENTDIFF->bottomRight.x), GAPSIZE))) { - SNAP(sourceX.end, sourceX.start, monX.end); - snaps |= SNAP_RIGHT; - } - if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && - ((HAS_TOP && canSnap(sourceY.start, monY.start, GAPSIZE)) || canSnap(sourceY.start, (monY.start -= MON->m_reservedArea.top() + EXTENTDIFF->topLeft.y), GAPSIZE))) { - SNAP(sourceY.start, sourceY.end, monY.start); - snaps |= SNAP_UP; - } - if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && - ((HAS_BOTTOM && canSnap(sourceY.end, monY.end, GAPSIZE)) || canSnap(sourceY.end, (monY.end += MON->m_reservedArea.bottom() + EXTENTDIFF->bottomRight.y), GAPSIZE))) { - SNAP(sourceY.end, sourceY.start, monY.end); - snaps |= SNAP_DOWN; - } - } - - // remove extents from main surface - sourceX = {sourceX.start + EXTENTS.topLeft.x, sourceX.end - EXTENTS.bottomRight.x}; - sourceY = {sourceY.start + EXTENTS.topLeft.y, sourceY.end - EXTENTS.bottomRight.y}; - - if (MODE == MBIND_RESIZE_FORCE_RATIO) { - if ((CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && snaps & SNAP_LEFT) || (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && snaps & SNAP_RIGHT)) { - const double SIZEY = (sourceX.end - sourceX.start) * (BEGINSIZE.y / BEGINSIZE.x); - if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT)) - sourceY.start = sourceY.end - SIZEY; - else - sourceY.end = sourceY.start + SIZEY; - } else if ((CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && snaps & SNAP_UP) || (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && snaps & SNAP_DOWN)) { - const double SIZEX = (sourceY.end - sourceY.start) * (BEGINSIZE.x / BEGINSIZE.y); - if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT)) - sourceX.start = sourceX.end - SIZEX; - else - sourceX.end = sourceX.start + SIZEX; - } - } - - sourcePos = {sourceX.start, sourceY.start}; - sourceSize = {sourceX.end - sourceX.start, sourceY.end - sourceY.start}; -} - -void IHyprLayout::onMouseMove(const Vector2D& mousePos) { - if (g_pInputManager->m_currentlyDraggedWindow.expired()) - return; - - const auto DRAGGINGWINDOW = g_pInputManager->m_currentlyDraggedWindow.lock(); - static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); - - // Window invalid or drag begin size 0,0 meaning we rejected it. - if ((!validMapped(DRAGGINGWINDOW) || m_beginDragSizeXY == Vector2D())) { - g_pKeybindManager->changeMouseBindMode(MBIND_INVALID); - return; - } - - // Yoink dragged window here instead if using drag_threshold and it has been reached - if (*PDRAGTHRESHOLD > 0 && !g_pInputManager->m_dragThresholdReached) { - if ((m_beginDragXY.distanceSq(mousePos) <= std::pow(*PDRAGTHRESHOLD, 2) && m_beginDragXY == m_lastDragXY)) - return; - g_pInputManager->m_dragThresholdReached = true; - if (updateDragWindow()) - return; - } - - static auto TIMER = std::chrono::high_resolution_clock::now(), MSTIMER = TIMER; - - const auto SPECIAL = DRAGGINGWINDOW->onSpecialWorkspace(); - - const auto DELTA = Vector2D(mousePos.x - m_beginDragXY.x, mousePos.y - m_beginDragXY.y); - const auto TICKDELTA = Vector2D(mousePos.x - m_lastDragXY.x, mousePos.y - m_lastDragXY.y); - - static auto PANIMATEMOUSE = CConfigValue("misc:animate_mouse_windowdragging"); - static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); - - static auto SNAPENABLED = CConfigValue("general:snap:enabled"); - - const auto TIMERDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - TIMER).count(); - const auto MSDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - MSTIMER).count(); - const auto MSMONITOR = 1000.0 / g_pHyprRenderer->m_mostHzMonitor->m_refreshRate; - static int totalMs = 0; - bool canSkipUpdate = true; - - MSTIMER = std::chrono::high_resolution_clock::now(); - - if (m_mouseMoveEventCount == 1) - totalMs = 0; - - if (MSMONITOR > 16.0) { - totalMs += MSDELTA < MSMONITOR ? MSDELTA : std::round(totalMs * 1.0 / m_mouseMoveEventCount); - m_mouseMoveEventCount += 1; - - // check if time-window is enough to skip update on 60hz monitor - canSkipUpdate = std::clamp(MSMONITOR - TIMERDELTA, 0.0, MSMONITOR) > totalMs * 1.0 / m_mouseMoveEventCount; - } - - if ((abs(TICKDELTA.x) < 1.f && abs(TICKDELTA.y) < 1.f) || (TIMERDELTA < MSMONITOR && canSkipUpdate && (g_pInputManager->m_dragMode != MBIND_MOVE || *PANIMATEMOUSE))) - return; - - TIMER = std::chrono::high_resolution_clock::now(); - - m_lastDragXY = mousePos; - - g_pHyprRenderer->damageWindow(DRAGGINGWINDOW); - - if (g_pInputManager->m_dragMode == MBIND_MOVE) { - - Vector2D newPos = m_beginDragPositionXY + DELTA; - Vector2D newSize = DRAGGINGWINDOW->m_realSize->goal(); - - if (*SNAPENABLED && !DRAGGINGWINDOW->m_draggingTiled) - performSnap(newPos, newSize, DRAGGINGWINDOW, MBIND_MOVE, -1, m_beginDragSizeXY); - - newPos = newPos.round(); - - if (*PANIMATEMOUSE) - *DRAGGINGWINDOW->m_realPosition = newPos; - else { - DRAGGINGWINDOW->m_realPosition->setValueAndWarp(newPos); - DRAGGINGWINDOW->sendWindowSize(); - } - - DRAGGINGWINDOW->m_position = newPos; - - } else if (g_pInputManager->m_dragMode == MBIND_RESIZE || g_pInputManager->m_dragMode == MBIND_RESIZE_FORCE_RATIO || g_pInputManager->m_dragMode == MBIND_RESIZE_BLOCK_RATIO) { - if (DRAGGINGWINDOW->m_isFloating) { - - Vector2D MINSIZE = DRAGGINGWINDOW->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}); - Vector2D MAXSIZE = DRAGGINGWINDOW->maxSize().value_or(Math::VECTOR2D_MAX); - - Vector2D newSize = m_beginDragSizeXY; - Vector2D newPos = m_beginDragPositionXY; - - if (m_grabbedCorner == CORNER_BOTTOMRIGHT) - newSize = newSize + DELTA; - else if (m_grabbedCorner == CORNER_TOPLEFT) - newSize = newSize - DELTA; - else if (m_grabbedCorner == CORNER_TOPRIGHT) - newSize = newSize + Vector2D(DELTA.x, -DELTA.y); - else if (m_grabbedCorner == CORNER_BOTTOMLEFT) - newSize = newSize + Vector2D(-DELTA.x, DELTA.y); - - eMouseBindMode mode = g_pInputManager->m_dragMode; - if (DRAGGINGWINDOW->m_ruleApplicator->keepAspectRatio().valueOrDefault() && mode != MBIND_RESIZE_BLOCK_RATIO) - mode = MBIND_RESIZE_FORCE_RATIO; - - if (m_beginDragSizeXY.x >= 1 && m_beginDragSizeXY.y >= 1 && mode == MBIND_RESIZE_FORCE_RATIO) { - - const float RATIO = m_beginDragSizeXY.y / m_beginDragSizeXY.x; - - if (MINSIZE.x * RATIO > MINSIZE.y) - MINSIZE = Vector2D(MINSIZE.x, MINSIZE.x * RATIO); - else - MINSIZE = Vector2D(MINSIZE.y / RATIO, MINSIZE.y); - - if (MAXSIZE.x * RATIO < MAXSIZE.y) - MAXSIZE = Vector2D(MAXSIZE.x, MAXSIZE.x * RATIO); - else - MAXSIZE = Vector2D(MAXSIZE.y / RATIO, MAXSIZE.y); - - if (newSize.x * RATIO > newSize.y) - newSize = Vector2D(newSize.x, newSize.x * RATIO); - else - newSize = Vector2D(newSize.y / RATIO, newSize.y); - } - - newSize = newSize.clamp(MINSIZE, MAXSIZE); - - if (m_grabbedCorner == CORNER_TOPLEFT) - newPos = newPos - newSize + m_beginDragSizeXY; - else if (m_grabbedCorner == CORNER_TOPRIGHT) - newPos = newPos + Vector2D(0.0, (m_beginDragSizeXY - newSize).y); - else if (m_grabbedCorner == CORNER_BOTTOMLEFT) - newPos = newPos + Vector2D((m_beginDragSizeXY - newSize).x, 0.0); - - if (*SNAPENABLED) { - performSnap(newPos, newSize, DRAGGINGWINDOW, mode, m_grabbedCorner, m_beginDragSizeXY); - newSize = newSize.clamp(MINSIZE, MAXSIZE); - } - - CBox wb = {newPos, newSize}; - wb.round(); - - if (*PANIMATE) { - *DRAGGINGWINDOW->m_realSize = wb.size(); - *DRAGGINGWINDOW->m_realPosition = wb.pos(); - } else { - DRAGGINGWINDOW->m_realSize->setValueAndWarp(wb.size()); - DRAGGINGWINDOW->m_realPosition->setValueAndWarp(wb.pos()); - DRAGGINGWINDOW->sendWindowSize(); - } - - DRAGGINGWINDOW->m_position = wb.pos(); - DRAGGINGWINDOW->m_size = wb.size(); - } else { - resizeActiveWindow(TICKDELTA, m_grabbedCorner, DRAGGINGWINDOW); - } - } - - // get middle point - Vector2D middle = DRAGGINGWINDOW->m_realPosition->value() + DRAGGINGWINDOW->m_realSize->value() / 2.f; - - // and check its monitor - const auto PMONITOR = g_pCompositor->getMonitorFromVector(middle); - - if (PMONITOR && !SPECIAL) { - DRAGGINGWINDOW->m_monitor = PMONITOR; - DRAGGINGWINDOW->moveToWorkspace(PMONITOR->m_activeWorkspace); - DRAGGINGWINDOW->updateGroupOutputs(); - - DRAGGINGWINDOW->updateToplevel(); - } - - DRAGGINGWINDOW->updateWindowDecos(); - - g_pHyprRenderer->damageWindow(DRAGGINGWINDOW); -} - -void IHyprLayout::changeWindowFloatingMode(PHLWINDOW pWindow) { - - if (pWindow->isFullscreen()) { - Log::logger->log(Log::DEBUG, "changeWindowFloatingMode: fullscreen"); - g_pCompositor->setWindowFullscreenInternal(pWindow, FSMODE_NONE); - } - - pWindow->m_pinned = false; - - g_pHyprRenderer->damageWindow(pWindow, true); - - const auto TILED = isWindowTiled(pWindow); - - // event - g_pEventManager->postEvent(SHyprIPCEvent{"changefloatingmode", std::format("{:x},{}", rc(pWindow.get()), sc(TILED))}); - EMIT_HOOK_EVENT("changeFloatingMode", pWindow); - - if (!TILED) { - const auto PNEWMON = g_pCompositor->getMonitorFromVector(pWindow->m_realPosition->value() + pWindow->m_realSize->value() / 2.f); - pWindow->m_monitor = PNEWMON; - pWindow->moveToWorkspace(PNEWMON->m_activeSpecialWorkspace ? PNEWMON->m_activeSpecialWorkspace : PNEWMON->m_activeWorkspace); - pWindow->updateGroupOutputs(); - - const auto PWORKSPACE = PNEWMON->m_activeSpecialWorkspace ? PNEWMON->m_activeSpecialWorkspace : PNEWMON->m_activeWorkspace; - - if (PWORKSPACE->m_hasFullscreenWindow) - g_pCompositor->setWindowFullscreenInternal(PWORKSPACE->getFullscreenWindow(), FSMODE_NONE); - - // save real pos cuz the func applies the default 5,5 mid - const auto PSAVEDPOS = pWindow->m_realPosition->goal(); - const auto PSAVEDSIZE = pWindow->m_realSize->goal(); - - // if the window is pseudo, update its size - if (!pWindow->m_draggingTiled) - pWindow->m_pseudoSize = pWindow->m_realSize->goal(); - - pWindow->m_lastFloatingSize = PSAVEDSIZE; - - // move to narnia because we don't wanna find our own node. onWindowCreatedTiling should apply the coords back. - pWindow->m_position = Vector2D(-999999, -999999); - - onWindowCreatedTiling(pWindow); - - pWindow->m_realPosition->setValue(PSAVEDPOS); - pWindow->m_realSize->setValue(PSAVEDSIZE); - - // fix pseudo leaving artifacts - g_pHyprRenderer->damageMonitor(pWindow->m_monitor.lock()); - - if (pWindow == Desktop::focusState()->window()) - m_lastTiledWindow = pWindow; - } else { - onWindowRemovedTiling(pWindow); - - g_pCompositor->changeWindowZOrder(pWindow, true); - - CBox wb = {pWindow->m_realPosition->goal() + (pWindow->m_realSize->goal() - pWindow->m_lastFloatingSize) / 2.f, pWindow->m_lastFloatingSize}; - wb.round(); - - if (!(pWindow->m_isFloating && pWindow->m_isPseudotiled) && DELTALESSTHAN(pWindow->m_realSize->goal().x, pWindow->m_lastFloatingSize.x, 10) && - DELTALESSTHAN(pWindow->m_realSize->goal().y, pWindow->m_lastFloatingSize.y, 10)) { - wb = {wb.pos() + Vector2D{10, 10}, wb.size() - Vector2D{20, 20}}; - } - - pWindow->m_size = wb.size(); - pWindow->m_position = wb.pos(); - - fitFloatingWindowOnMonitor(pWindow, wb); - - g_pHyprRenderer->damageMonitor(pWindow->m_monitor.lock()); - - pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - pWindow->updateWindowData(); - - if (pWindow == m_lastTiledWindow) - m_lastTiledWindow.reset(); - } - - pWindow->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_ON_WORKSPACE | Desktop::Rule::RULE_PROP_FLOATING); - pWindow->updateDecorationValues(); - pWindow->updateToplevel(); - g_pHyprRenderer->damageWindow(pWindow); -} - -void IHyprLayout::fitFloatingWindowOnMonitor(PHLWINDOW w, std::optional tb) { - if (!w->m_isFloating) - return; - - const auto PMONITOR = w->m_monitor.lock(); - - if (!PMONITOR) - return; - - const auto EXTENTS = w->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); - CBox targetBoxMonLocal = tb.value_or(w->getWindowMainSurfaceBox()).translate(-PMONITOR->m_position).addExtents(EXTENTS); - const auto MONITOR_LOCAL_BOX = PMONITOR->logicalBoxMinusReserved().translate(-PMONITOR->m_position); - - if (targetBoxMonLocal.w < MONITOR_LOCAL_BOX.w) { - if (targetBoxMonLocal.x < MONITOR_LOCAL_BOX.x) - targetBoxMonLocal.x = MONITOR_LOCAL_BOX.x; - else if (targetBoxMonLocal.x + targetBoxMonLocal.w > MONITOR_LOCAL_BOX.w) - targetBoxMonLocal.x = MONITOR_LOCAL_BOX.w - targetBoxMonLocal.w; - } - - if (targetBoxMonLocal.h < MONITOR_LOCAL_BOX.h) { - if (targetBoxMonLocal.y < MONITOR_LOCAL_BOX.y) - targetBoxMonLocal.y = MONITOR_LOCAL_BOX.y; - else if (targetBoxMonLocal.y + targetBoxMonLocal.h > MONITOR_LOCAL_BOX.h) - targetBoxMonLocal.y = MONITOR_LOCAL_BOX.h - targetBoxMonLocal.h; - } - - *w->m_realPosition = (targetBoxMonLocal.pos() + PMONITOR->m_position + EXTENTS.topLeft).round(); - *w->m_realSize = (targetBoxMonLocal.size() - EXTENTS.topLeft - EXTENTS.bottomRight).round(); -} - -void IHyprLayout::moveActiveWindow(const Vector2D& delta, PHLWINDOW pWindow) { - const auto PWINDOW = pWindow ? pWindow : Desktop::focusState()->window(); - - if (!validMapped(PWINDOW)) - return; - - if (!PWINDOW->m_isFloating) { - Log::logger->log(Log::DEBUG, "Dwindle cannot move a tiled window in moveActiveWindow!"); - return; - } - - PWINDOW->setAnimationsToMove(); - - PWINDOW->m_position += delta; - *PWINDOW->m_realPosition = PWINDOW->m_realPosition->goal() + delta; - - g_pHyprRenderer->damageWindow(PWINDOW); -} - -void IHyprLayout::onWindowFocusChange(PHLWINDOW pNewFocus) { - m_lastTiledWindow = pNewFocus && !pNewFocus->m_isFloating ? pNewFocus : m_lastTiledWindow; -} - -PHLWINDOW IHyprLayout::getNextWindowCandidate(PHLWINDOW pWindow) { - // although we don't expect nullptrs here, let's verify jic - if (!pWindow) - return nullptr; - - const auto PWORKSPACE = pWindow->m_workspace; - - // first of all, if this is a fullscreen workspace, - if (PWORKSPACE->m_hasFullscreenWindow) - return PWORKSPACE->getFullscreenWindow(); - - if (pWindow->m_isFloating) { - - // find whether there is a floating window below this one - for (auto const& w : g_pCompositor->m_windows) { - if (w->m_isMapped && !w->isHidden() && w->m_isFloating && !w->isX11OverrideRedirect() && w->m_workspace == pWindow->m_workspace && !w->m_X11ShouldntFocus && - !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pWindow) { - if (VECINRECT((pWindow->m_size / 2.f + pWindow->m_position), w->m_position.x, w->m_position.y, w->m_position.x + w->m_size.x, w->m_position.y + w->m_size.y)) { - return w; - } - } - } - - // let's try the last tiled window. - if (m_lastTiledWindow.lock() && m_lastTiledWindow->m_workspace == pWindow->m_workspace) - return m_lastTiledWindow.lock(); - - // if we don't, let's try to find any window that is in the middle - if (const auto PWINDOWCANDIDATE = - g_pCompositor->vectorToWindowUnified(pWindow->middle(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING); - PWINDOWCANDIDATE && PWINDOWCANDIDATE != pWindow) - return PWINDOWCANDIDATE; - - // if not, floating window - for (auto const& w : g_pCompositor->m_windows) { - if (w->m_isMapped && !w->isHidden() && w->m_isFloating && !w->isX11OverrideRedirect() && w->m_workspace == pWindow->m_workspace && !w->m_X11ShouldntFocus && - !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pWindow) - return w; - } - - // if there is no candidate, too bad - return nullptr; - } - - // if it was a tiled window, we first try to find the window that will replace it. - auto pWindowCandidate = g_pCompositor->vectorToWindowUnified(pWindow->middle(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING); - - if (!pWindowCandidate) - pWindowCandidate = PWORKSPACE->getTopLeftWindow(); - - if (!pWindowCandidate) - pWindowCandidate = PWORKSPACE->getFirstWindow(); - - if (!pWindowCandidate || pWindow == pWindowCandidate || !pWindowCandidate->m_isMapped || pWindowCandidate->isHidden() || pWindowCandidate->m_X11ShouldntFocus || - pWindowCandidate->isX11OverrideRedirect() || pWindowCandidate->m_monitor != Desktop::focusState()->monitor()) - return nullptr; - - return pWindowCandidate; -} - -bool IHyprLayout::isWindowReachable(PHLWINDOW pWindow) { - return pWindow && (!pWindow->isHidden() || pWindow->m_groupData.pNextWindow); -} - -void IHyprLayout::bringWindowToTop(PHLWINDOW pWindow) { - if (pWindow == nullptr) - return; - - if (pWindow->isHidden() && pWindow->m_groupData.pNextWindow) { - // grouped, change the current to this window - pWindow->setGroupCurrent(pWindow); - } -} - -void IHyprLayout::requestFocusForWindow(PHLWINDOW pWindow) { - bringWindowToTop(pWindow); - Desktop::focusState()->fullWindowFocus(pWindow); - g_pCompositor->warpCursorTo(pWindow->middle()); -} - -Vector2D IHyprLayout::predictSizeForNewWindowFloating(PHLWINDOW pWindow) { // get all rules, see if we have any size overrides. - Vector2D sizeOverride = {}; - if (Desktop::focusState()->monitor()) { - - // If `persistentsize` is set, use the stored size if available. - const bool HASPERSISTENTSIZE = pWindow->m_ruleApplicator->persistentSize().valueOrDefault(); - - const auto STOREDSIZE = HASPERSISTENTSIZE ? g_pConfigManager->getStoredFloatingSize(pWindow) : std::nullopt; - - if (STOREDSIZE.has_value()) { - Log::logger->log(Log::DEBUG, "using stored size {}x{} for new floating window {}::{}", STOREDSIZE->x, STOREDSIZE->y, pWindow->m_class, pWindow->m_title); - return STOREDSIZE.value(); - } - - if (!pWindow->m_ruleApplicator->static_.size.empty()) { - const auto SIZE = pWindow->calculateExpression(pWindow->m_ruleApplicator->static_.size); - if (SIZE) - return SIZE.value(); - } - } - - return sizeOverride; -} - -Vector2D IHyprLayout::predictSizeForNewWindow(PHLWINDOW pWindow) { - bool shouldBeFloated = g_pXWaylandManager->shouldBeFloated(pWindow, true) || pWindow->m_ruleApplicator->static_.floating.value_or(false); - - Vector2D sizePredicted = {}; - - if (!shouldBeFloated) - sizePredicted = predictSizeForNewWindowTiled(); - else - sizePredicted = predictSizeForNewWindowFloating(pWindow); - - Vector2D maxSize = pWindow->m_xdgSurface->m_toplevel->m_pending.maxSize; - - if ((maxSize.x > 0 && maxSize.x < sizePredicted.x) || (maxSize.y > 0 && maxSize.y < sizePredicted.y)) - sizePredicted = {}; - - return sizePredicted; -} - -bool IHyprLayout::updateDragWindow() { - const auto DRAGGINGWINDOW = g_pInputManager->m_currentlyDraggedWindow.lock(); - const bool WAS_FULLSCREEN = DRAGGINGWINDOW->isFullscreen(); - - if (g_pInputManager->m_dragThresholdReached) { - if (WAS_FULLSCREEN) { - Log::logger->log(Log::DEBUG, "Dragging a fullscreen window"); - g_pCompositor->setWindowFullscreenInternal(DRAGGINGWINDOW, FSMODE_NONE); - } - - const auto PWORKSPACE = DRAGGINGWINDOW->m_workspace; - - if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGWINDOW->m_isFloating || (!DRAGGINGWINDOW->m_createdOverFullscreen && !DRAGGINGWINDOW->m_pinned))) { - Log::logger->log(Log::DEBUG, "Rejecting drag on a fullscreen workspace. (window under fullscreen)"); - g_pKeybindManager->changeMouseBindMode(MBIND_INVALID); - return true; - } - } - - DRAGGINGWINDOW->m_draggingTiled = false; - m_draggingWindowOriginalFloatSize = DRAGGINGWINDOW->m_lastFloatingSize; - - if (WAS_FULLSCREEN && DRAGGINGWINDOW->m_isFloating) { - const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); - *DRAGGINGWINDOW->m_realPosition = MOUSECOORDS - DRAGGINGWINDOW->m_realSize->goal() / 2.f; - } else if (!DRAGGINGWINDOW->m_isFloating && g_pInputManager->m_dragMode == MBIND_MOVE) { - Vector2D MINSIZE = DRAGGINGWINDOW->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}); - DRAGGINGWINDOW->m_lastFloatingSize = (DRAGGINGWINDOW->m_realSize->goal() * 0.8489).clamp(MINSIZE, Vector2D{}).floor(); - *DRAGGINGWINDOW->m_realPosition = g_pInputManager->getMouseCoordsInternal() - DRAGGINGWINDOW->m_realSize->goal() / 2.f; - if (g_pInputManager->m_dragThresholdReached) { - changeWindowFloatingMode(DRAGGINGWINDOW); - DRAGGINGWINDOW->m_isFloating = true; - DRAGGINGWINDOW->m_draggingTiled = true; - } - } - - m_beginDragXY = g_pInputManager->getMouseCoordsInternal(); - m_beginDragPositionXY = DRAGGINGWINDOW->m_realPosition->goal(); - m_beginDragSizeXY = DRAGGINGWINDOW->m_realSize->goal(); - m_lastDragXY = m_beginDragXY; - - return false; -} - -CBox IHyprLayout::workAreaOnWorkspace(const PHLWORKSPACE& pWorkspace) { - if (!pWorkspace || !pWorkspace->m_monitor) - return {}; - - const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(pWorkspace); - - auto workArea = pWorkspace->m_monitor->logicalBoxMinusReserved(); - - static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); - auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())->getData()); - auto gapsOut = WORKSPACERULE.gapsOut.value_or(*PGAPSOUT); - - Desktop::CReservedArea reservedGaps{gapsOut.m_top, gapsOut.m_right, gapsOut.m_bottom, gapsOut.m_left}; - - reservedGaps.applyip(workArea); - - return workArea; -} diff --git a/src/layout/IHyprLayout.hpp b/src/layout/IHyprLayout.hpp deleted file mode 100644 index 0b23bc3b..00000000 --- a/src/layout/IHyprLayout.hpp +++ /dev/null @@ -1,248 +0,0 @@ -#pragma once - -#include "../defines.hpp" -#include "../managers/input/InputManager.hpp" -#include - -class CGradientValueData; - -struct SWindowRenderLayoutHints { - bool isBorderGradient = false; - CGradientValueData* borderGradient = nullptr; -}; - -struct SLayoutMessageHeader { - PHLWINDOW pWindow; -}; - -enum eFullscreenMode : int8_t; - -enum eRectCorner : uint8_t { - CORNER_NONE = 0, - CORNER_TOPLEFT = (1 << 0), - CORNER_TOPRIGHT = (1 << 1), - CORNER_BOTTOMRIGHT = (1 << 2), - CORNER_BOTTOMLEFT = (1 << 3), -}; - -inline eRectCorner cornerFromBox(const CBox& box, const Vector2D& pos) { - const auto CENTER = box.middle(); - - if (pos.x < CENTER.x) - return pos.y < CENTER.y ? CORNER_TOPLEFT : CORNER_BOTTOMLEFT; - return pos.y < CENTER.y ? CORNER_TOPRIGHT : CORNER_BOTTOMRIGHT; -} - -enum eSnapEdge : uint8_t { - SNAP_INVALID = 0, - SNAP_UP = (1 << 0), - SNAP_DOWN = (1 << 1), - SNAP_LEFT = (1 << 2), - SNAP_RIGHT = (1 << 3), -}; - -enum eDirection : int8_t { - DIRECTION_DEFAULT = -1, - DIRECTION_UP = 0, - DIRECTION_RIGHT, - DIRECTION_DOWN, - DIRECTION_LEFT -}; - -class IHyprLayout { - public: - virtual ~IHyprLayout() = default; - virtual void onEnable() = 0; - virtual void onDisable() = 0; - - /* - Called when a window is created (mapped) - The layout HAS TO set the goal pos and size (anim mgr will use it) - If !animationinprogress, then the anim mgr will not apply an anim. - */ - virtual void onWindowCreated(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT); - virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT) = 0; - virtual void onWindowCreatedFloating(PHLWINDOW); - virtual bool onWindowCreatedAutoGroup(PHLWINDOW); - - /* - Return tiled status - */ - virtual bool isWindowTiled(PHLWINDOW) = 0; - - /* - Called when a window is removed (unmapped) - */ - virtual void onWindowRemoved(PHLWINDOW); - virtual void onWindowRemovedTiling(PHLWINDOW) = 0; - virtual void onWindowRemovedFloating(PHLWINDOW); - /* - Called when the monitor requires a layout recalculation - this usually means reserved area changes - */ - virtual void recalculateMonitor(const MONITORID&) = 0; - - /* - Called when the compositor requests a window - to be recalculated, e.g. when pseudo is toggled. - */ - virtual void recalculateWindow(PHLWINDOW) = 0; - - /* - Called when a window is requested to be floated - */ - virtual void changeWindowFloatingMode(PHLWINDOW); - /* - Called when a window is clicked on, beginning a drag - this might be a resize, move, whatever the layout defines it - as. - */ - virtual void onBeginDragWindow(); - /* - Called when a user requests a resize of the current window by a vec - Vector2D holds pixel values - Optional pWindow for a specific window - */ - virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr) = 0; - /* - Called when a user requests a move of the current window by a vec - Vector2D holds pixel values - Optional pWindow for a specific window - */ - virtual void moveActiveWindow(const Vector2D&, PHLWINDOW pWindow = nullptr); - /* - Called when a window is ended being dragged - (mouse up) - */ - virtual void onEndDragWindow(); - /* - Called whenever the mouse moves, should the layout want to - do anything with it. - Useful for dragging. - */ - virtual void onMouseMove(const Vector2D&); - - /* - Called when a window / the user requests to toggle the fullscreen state of a window - The layout sets all the fullscreen flags. - It can either accept or ignore. - */ - virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) = 0; - - /* - Called when a dispatcher requests a custom message - The layout is free to ignore. - std::any is the reply. Can be empty. - */ - virtual std::any layoutMessage(SLayoutMessageHeader, std::string) = 0; - - /* - Required to be handled, but may return just SWindowRenderLayoutHints() - Called when the renderer requests any special draw flags for - a specific window, e.g. border color for groups. - */ - virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW) = 0; - - /* - Called when the user requests two windows to be swapped places. - The layout is free to ignore. - */ - virtual void switchWindows(PHLWINDOW, PHLWINDOW) = 0; - - /* - Called when the user requests a window move in a direction. - The layout is free to ignore. - */ - virtual void moveWindowTo(PHLWINDOW, const std::string& direction, bool silent = false) = 0; - - /* - Called when the user requests to change the splitratio by or to X - on a window - */ - virtual void alterSplitRatio(PHLWINDOW, float, bool exact = false) = 0; - - /* - Called when something wants the current layout's name - */ - virtual std::string getLayoutName() = 0; - - /* - Called for getting the next candidate for a focus - */ - virtual PHLWINDOW getNextWindowCandidate(PHLWINDOW); - - /* - Internal: called when window focus changes - */ - virtual void onWindowFocusChange(PHLWINDOW); - - /* - Called for replacing any data a layout has for a new window - */ - virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) = 0; - - /* - Determines if a window can be focused. If hidden this usually means the window is part of a group. - */ - virtual bool isWindowReachable(PHLWINDOW); - - /* - Called before an attempt is made to focus a window. - Brings the window to the top of any groups and ensures it is not hidden. - If the window is unmapped following this call, the focus attempt will fail. - */ - virtual void bringWindowToTop(PHLWINDOW); - - /* - Called via the foreign toplevel activation protocol. - Focuses a window, bringing it to the top of its group if applicable. - May be ignored. - */ - virtual void requestFocusForWindow(PHLWINDOW); - - /* - Called to predict the size of a newly opened window to send it a configure. - Return 0,0 if unpredictable - */ - virtual Vector2D predictSizeForNewWindowTiled() = 0; - - /* - Prefer not overriding, use predictSizeForNewWindowTiled. - */ - virtual Vector2D predictSizeForNewWindow(PHLWINDOW pWindow); - virtual Vector2D predictSizeForNewWindowFloating(PHLWINDOW pWindow); - - /* - Called to try to pick up window for dragging. - Updates drag related variables and floats window if threshold reached. - Return true to reject - */ - virtual bool updateDragWindow(); - - /* - Triggers a window snap event - */ - virtual void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, PHLWINDOW DRAGGINGWINDOW, const eMouseBindMode MODE, const int CORNER, const Vector2D& BEGINSIZE); - - /* - Fits a floating window on its monitor - */ - virtual void fitFloatingWindowOnMonitor(PHLWINDOW w, std::optional targetBox = std::nullopt); - - /* - Returns a logical box describing the work area on a workspace - (monitor size - reserved - gapsOut) - */ - virtual CBox workAreaOnWorkspace(const PHLWORKSPACE& pWorkspace); - - private: - int m_mouseMoveEventCount; - Vector2D m_beginDragXY; - Vector2D m_lastDragXY; - Vector2D m_beginDragPositionXY; - Vector2D m_beginDragSizeXY; - Vector2D m_draggingWindowOriginalFloatSize; - eRectCorner m_grabbedCorner = CORNER_TOPLEFT; - - PHLWINDOWREF m_lastTiledWindow; -}; diff --git a/src/layout/LayoutManager.cpp b/src/layout/LayoutManager.cpp new file mode 100644 index 00000000..29caa0ea --- /dev/null +++ b/src/layout/LayoutManager.cpp @@ -0,0 +1,344 @@ +#include "LayoutManager.hpp" + +#include "space/Space.hpp" +#include "target/Target.hpp" + +#include "../config/ConfigManager.hpp" +#include "../Compositor.hpp" +#include "../managers/HookSystemManager.hpp" +#include "../desktop/state/FocusState.hpp" +#include "../desktop/view/Group.hpp" + +using namespace Layout; + +CLayoutManager::CLayoutManager() { + static auto P = g_pHookSystem->hookDynamic("monitorLayoutChanged", [](void* hk, SCallbackInfo& info, std::any param) { + for (const auto& ws : g_pCompositor->getWorkspaces()) { + ws->m_space->recheckWorkArea(); + } + }); +} + +void CLayoutManager::newTarget(SP target, SP space) { + // on a new target: remember desired pos for float, if available + if (const auto DESIRED_GEOM = target->desiredGeometry(); DESIRED_GEOM) + target->rememberFloatingSize(DESIRED_GEOM->size); + + target->assignToSpace(space); +} + +void CLayoutManager::removeTarget(SP target) { + target->assignToSpace(nullptr); +} + +void CLayoutManager::changeFloatingMode(SP target) { + if (!target->space()) + return; + + target->space()->toggleTargetFloating(target); +} + +void CLayoutManager::beginDragTarget(SP target, eMouseBindMode mode) { + m_dragStateController->dragBegin(target, mode); +} + +void CLayoutManager::moveMouse(const Vector2D& mousePos) { + m_dragStateController->mouseMove(mousePos); +} + +void CLayoutManager::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + if (target->isPseudo()) { + auto fixedΔ = Δ; + if (corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT) + fixedΔ.x = -fixedΔ.x; + if (corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT) + fixedΔ.y = -fixedΔ.y; + + auto newPseudoSize = target->pseudoSize() + fixedΔ; + const auto TARGET_TILE_SIZE = target->position().size(); + newPseudoSize.x = std::clamp(newPseudoSize.x, MIN_WINDOW_SIZE, TARGET_TILE_SIZE.x); + newPseudoSize.y = std::clamp(newPseudoSize.y, MIN_WINDOW_SIZE, TARGET_TILE_SIZE.y); + + target->setPseudoSize(newPseudoSize); + + return; + } + + target->space()->resizeTarget(Δ, target, corner); +} + +std::expected CLayoutManager::layoutMsg(const std::string_view& sv) { + + const auto MONITOR = Desktop::focusState()->monitor(); + // forward to the active workspace + if (!MONITOR) + return std::unexpected("No monitor, can't find ws to target"); + + auto ws = MONITOR->m_activeSpecialWorkspace ? MONITOR->m_activeSpecialWorkspace : MONITOR->m_activeWorkspace; + + if (!ws) + return std::unexpected("No workspace, can't target"); + + return ws->m_space->layoutMsg(sv); +} + +void CLayoutManager::moveTarget(const Vector2D& Δ, SP target) { + if (!target->floating()) + return; + + target->space()->moveTarget(Δ, target); +} + +void CLayoutManager::endDragTarget() { + m_dragStateController->dragEnd(); +} + +void CLayoutManager::fullscreenRequestForTarget(SP target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode) { + target->space()->setFullscreen(target, effectiveMode); +} + +void CLayoutManager::switchTargets(SP a, SP b, bool preserveFocus) { + + if (preserveFocus) { + a->swap(b); + return; + } + + const auto IS_A_ACTIVE = Desktop::focusState()->window() == a->window(); + const auto IS_B_ACTIVE = Desktop::focusState()->window() == b->window(); + + a->swap(b); + + if (IS_A_ACTIVE && b->window()) + Desktop::focusState()->fullWindowFocus(b->window(), Desktop::FOCUS_REASON_KEYBIND); + + if (IS_B_ACTIVE && a->window()) + Desktop::focusState()->fullWindowFocus(a->window(), Desktop::FOCUS_REASON_KEYBIND); +} + +void CLayoutManager::moveInDirection(SP target, const std::string& direction, bool silent) { + Math::eDirection dir = Math::fromChar(direction.at(0)); + if (dir == Math::DIRECTION_DEFAULT) { + Log::logger->log(Log::ERR, "invalid direction for moveInDirection: {}", direction); + return; + } + + target->space()->moveTargetInDirection(target, dir, silent); +} + +SP CLayoutManager::getNextCandidate(SP space, SP from) { + return space->getNextCandidate(from); +} + +bool CLayoutManager::isReachable(SP target) { + return true; +} + +void CLayoutManager::bringTargetToTop(SP target) { + if (!target) + return; + + if (target->window()->m_group) { + // grouped, change the current to this window + target->window()->m_group->setCurrent(target->window()); + } +} + +std::optional CLayoutManager::predictSizeForNewTiledTarget() { + const auto FOCUSED_MON = Desktop::focusState()->monitor(); + + if (!FOCUSED_MON || !FOCUSED_MON->m_activeWorkspace) + return std::nullopt; + + if (FOCUSED_MON->m_activeSpecialWorkspace) + return FOCUSED_MON->m_activeSpecialWorkspace->m_space->predictSizeForNewTiledTarget(); + + return FOCUSED_MON->m_activeWorkspace->m_space->predictSizeForNewTiledTarget(); +} + +const UP& CLayoutManager::dragController() { + return m_dragStateController; +} + +static inline bool canSnap(const double SIDEA, const double SIDEB, const double GAP) { + return std::abs(SIDEA - SIDEB) < GAP; +} + +static void snapMove(double& start, double& end, const double P) { + end = P + (end - start); + start = P; +} + +static void snapResize(double& start, double& end, const double P) { + start = P; +} + +using SnapFn = std::function; + +void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP DRAGGINGTARGET, const eMouseBindMode MODE, const int CORNER, const Vector2D& BEGINSIZE) { + + const auto DRAGGINGWINDOW = DRAGGINGTARGET->window(); + + if (!Desktop::View::validMapped(DRAGGINGWINDOW)) + return; + + static auto SNAPWINDOWGAP = CConfigValue("general:snap:window_gap"); + static auto SNAPMONITORGAP = CConfigValue("general:snap:monitor_gap"); + static auto SNAPBORDEROVERLAP = CConfigValue("general:snap:border_overlap"); + static auto SNAPRESPECTGAPS = CConfigValue("general:snap:respect_gaps"); + + static auto PGAPSIN = CConfigValue("general:gaps_in"); + static auto PGAPSOUT = CConfigValue("general:gaps_out"); + const auto GAPSNONE = CCssGapData{0, 0, 0, 0}; + + const SnapFn SNAP = (MODE == MBIND_MOVE) ? snapMove : snapResize; + int snaps = 0; + + struct SRange { + double start = 0; + double end = 0; + }; + const auto EXTENTS = DRAGGINGWINDOW->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); + SRange sourceX = {sourcePos.x - EXTENTS.topLeft.x, sourcePos.x + sourceSize.x + EXTENTS.bottomRight.x}; + SRange sourceY = {sourcePos.y - EXTENTS.topLeft.y, sourcePos.y + sourceSize.y + EXTENTS.bottomRight.y}; + + if (*SNAPWINDOWGAP) { + const double GAPSIZE = *SNAPWINDOWGAP; + const auto WSID = DRAGGINGWINDOW->workspaceID(); + const bool HASFULLSCREEN = DRAGGINGWINDOW->m_workspace && DRAGGINGWINDOW->m_workspace->m_hasFullscreenWindow; + + const auto* GAPSIN = *SNAPRESPECTGAPS ? sc(PGAPSIN.ptr()->getData()) : &GAPSNONE; + const double GAPSX = GAPSIN->m_left + GAPSIN->m_right; + const double GAPSY = GAPSIN->m_top + GAPSIN->m_bottom; + + for (auto& other : g_pCompositor->m_windows) { + if ((HASFULLSCREEN && !other->m_createdOverFullscreen) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut || + other->isX11OverrideRedirect()) + continue; + + const CBox SURF = other->getWindowBoxUnified(Desktop::View::RESERVED_EXTENTS); + const SRange SURFBX = {SURF.x - GAPSX, SURF.x + SURF.w + GAPSX}; + const SRange SURFBY = {SURF.y - GAPSY, SURF.y + SURF.h + GAPSY}; + + // only snap windows if their ranges overlap in the opposite axis + if (sourceY.start <= SURFBY.end && SURFBY.start <= sourceY.end) { + if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && canSnap(sourceX.start, SURFBX.end, GAPSIZE)) { + SNAP(sourceX.start, sourceX.end, SURFBX.end); + snaps |= SNAP_LEFT; + } else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && canSnap(sourceX.end, SURFBX.start, GAPSIZE)) { + SNAP(sourceX.end, sourceX.start, SURFBX.start); + snaps |= SNAP_RIGHT; + } + } + if (sourceX.start <= SURFBX.end && SURFBX.start <= sourceX.end) { + if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && canSnap(sourceY.start, SURFBY.end, GAPSIZE)) { + SNAP(sourceY.start, sourceY.end, SURFBY.end); + snaps |= SNAP_UP; + } else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && canSnap(sourceY.end, SURFBY.start, GAPSIZE)) { + SNAP(sourceY.end, sourceY.start, SURFBY.start); + snaps |= SNAP_DOWN; + } + } + + // corner snapping + if (sourceX.start == SURFBX.end || SURFBX.start == sourceX.end) { + const SRange SURFY = {SURFBY.start + GAPSY, SURFBY.end - GAPSY}; + if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && !(snaps & SNAP_UP) && canSnap(sourceY.start, SURFY.start, GAPSIZE)) { + SNAP(sourceY.start, sourceY.end, SURFY.start); + snaps |= SNAP_UP; + } else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_DOWN) && canSnap(sourceY.end, SURFY.end, GAPSIZE)) { + SNAP(sourceY.end, sourceY.start, SURFY.end); + snaps |= SNAP_DOWN; + } + } + if (sourceY.start == SURFBY.end || SURFBY.start == sourceY.end) { + const SRange SURFX = {SURFBX.start + GAPSX, SURFBX.end - GAPSX}; + if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && !(snaps & SNAP_LEFT) && canSnap(sourceX.start, SURFX.start, GAPSIZE)) { + SNAP(sourceX.start, sourceX.end, SURFX.start); + snaps |= SNAP_LEFT; + } else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_RIGHT) && canSnap(sourceX.end, SURFX.end, GAPSIZE)) { + SNAP(sourceX.end, sourceX.start, SURFX.end); + snaps |= SNAP_RIGHT; + } + } + } + } + + if (*SNAPMONITORGAP) { + const double GAPSIZE = *SNAPMONITORGAP; + const auto EXTENTNONE = SBoxExtents{{0, 0}, {0, 0}}; + const auto* EXTENTDIFF = *SNAPBORDEROVERLAP ? &EXTENTS : &EXTENTNONE; + const auto MON = DRAGGINGWINDOW->m_monitor.lock(); + + const auto* GAPSOUT = *SNAPRESPECTGAPS ? sc(PGAPSOUT.ptr()->getData()) : &GAPSNONE; + const auto WORK_AREA = Desktop::CReservedArea{GAPSOUT->m_top, GAPSOUT->m_right, GAPSOUT->m_bottom, GAPSOUT->m_left}.apply(MON->logicalBoxMinusReserved()); + + SRange monX = {WORK_AREA.x, WORK_AREA.x + WORK_AREA.w}; + SRange monY = {WORK_AREA.y, WORK_AREA.y + WORK_AREA.h}; + + const bool HAS_LEFT = MON->m_reservedArea.left() > 0; + const bool HAS_TOP = MON->m_reservedArea.top() > 0; + const bool HAS_BOTTOM = MON->m_reservedArea.bottom() > 0; + const bool HAS_RIGHT = MON->m_reservedArea.right() > 0; + + if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && + ((HAS_LEFT && canSnap(sourceX.start, monX.start, GAPSIZE)) || canSnap(sourceX.start, (monX.start -= MON->m_reservedArea.left() + EXTENTDIFF->topLeft.x), GAPSIZE))) { + SNAP(sourceX.start, sourceX.end, monX.start); + snaps |= SNAP_LEFT; + } + if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && + ((HAS_RIGHT && canSnap(sourceX.end, monX.end, GAPSIZE)) || canSnap(sourceX.end, (monX.end += MON->m_reservedArea.right() + EXTENTDIFF->bottomRight.x), GAPSIZE))) { + SNAP(sourceX.end, sourceX.start, monX.end); + snaps |= SNAP_RIGHT; + } + if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && + ((HAS_TOP && canSnap(sourceY.start, monY.start, GAPSIZE)) || canSnap(sourceY.start, (monY.start -= MON->m_reservedArea.top() + EXTENTDIFF->topLeft.y), GAPSIZE))) { + SNAP(sourceY.start, sourceY.end, monY.start); + snaps |= SNAP_UP; + } + if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && + ((HAS_BOTTOM && canSnap(sourceY.end, monY.end, GAPSIZE)) || canSnap(sourceY.end, (monY.end += MON->m_reservedArea.bottom() + EXTENTDIFF->bottomRight.y), GAPSIZE))) { + SNAP(sourceY.end, sourceY.start, monY.end); + snaps |= SNAP_DOWN; + } + } + + // remove extents from main surface + sourceX = {sourceX.start + EXTENTS.topLeft.x, sourceX.end - EXTENTS.bottomRight.x}; + sourceY = {sourceY.start + EXTENTS.topLeft.y, sourceY.end - EXTENTS.bottomRight.y}; + + if (MODE == MBIND_RESIZE_FORCE_RATIO) { + if ((CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && snaps & SNAP_LEFT) || (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && snaps & SNAP_RIGHT)) { + const double SIZEY = (sourceX.end - sourceX.start) * (BEGINSIZE.y / BEGINSIZE.x); + if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT)) + sourceY.start = sourceY.end - SIZEY; + else + sourceY.end = sourceY.start + SIZEY; + } else if ((CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && snaps & SNAP_UP) || (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && snaps & SNAP_DOWN)) { + const double SIZEX = (sourceY.end - sourceY.start) * (BEGINSIZE.x / BEGINSIZE.y); + if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT)) + sourceX.start = sourceX.end - SIZEX; + else + sourceX.end = sourceX.start + SIZEX; + } + } + + sourcePos = {sourceX.start, sourceY.start}; + sourceSize = {sourceX.end - sourceX.start, sourceY.end - sourceY.start}; +} + +void CLayoutManager::recalculateMonitor(PHLMONITOR m) { + if (m->m_activeSpecialWorkspace) + m->m_activeSpecialWorkspace->m_space->recalculate(); + if (m->m_activeWorkspace) + m->m_activeWorkspace->m_space->recalculate(); +} + +void CLayoutManager::invalidateMonitorGeometries(PHLMONITOR m) { + for (const auto& ws : g_pCompositor->getWorkspaces()) { + if (ws && ws->m_monitor == m) { + ws->m_space->recheckWorkArea(); + ws->m_space->recalculate(); + } + } +} diff --git a/src/layout/LayoutManager.hpp b/src/layout/LayoutManager.hpp new file mode 100644 index 00000000..638c9f4c --- /dev/null +++ b/src/layout/LayoutManager.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "../helpers/memory/Memory.hpp" +#include "../helpers/math/Math.hpp" +#include "../managers/input/InputManager.hpp" + +#include "supplementary/DragController.hpp" + +#include +#include + +enum eFullscreenMode : int8_t; + +namespace Layout { + class ITarget; + class CSpace; + + enum eRectCorner : uint8_t { + CORNER_NONE = 0, + CORNER_TOPLEFT = (1 << 0), + CORNER_TOPRIGHT = (1 << 1), + CORNER_BOTTOMRIGHT = (1 << 2), + CORNER_BOTTOMLEFT = (1 << 3), + }; + + inline eRectCorner cornerFromBox(const CBox& box, const Vector2D& pos) { + const auto CENTER = box.middle(); + + if (pos.x < CENTER.x) + return pos.y < CENTER.y ? CORNER_TOPLEFT : CORNER_BOTTOMLEFT; + return pos.y < CENTER.y ? CORNER_TOPRIGHT : CORNER_BOTTOMRIGHT; + } + + enum eSnapEdge : uint8_t { + SNAP_INVALID = 0, + SNAP_UP = (1 << 0), + SNAP_DOWN = (1 << 1), + SNAP_LEFT = (1 << 2), + SNAP_RIGHT = (1 << 3), + }; + + class CLayoutManager { + public: + CLayoutManager(); + ~CLayoutManager() = default; + + void newTarget(SP target, SP space); + void removeTarget(SP target); + + void changeFloatingMode(SP target); + + void beginDragTarget(SP target, eMouseBindMode mode); + void moveMouse(const Vector2D& mousePos); + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); + void endDragTarget(); + + std::expected layoutMsg(const std::string_view& sv); + + void fullscreenRequestForTarget(SP target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode); + + void switchTargets(SP a, SP b, bool preserveFocus = true); + + void moveInDirection(SP target, const std::string& direction, bool silent = false); + + SP getNextCandidate(SP space, SP from); + + bool isReachable(SP target); + + void bringTargetToTop(SP target); + + std::optional predictSizeForNewTiledTarget(); + + void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP target, eMouseBindMode mode, int corner, const Vector2D& beginSize); + + void invalidateMonitorGeometries(PHLMONITOR); + void recalculateMonitor(PHLMONITOR); + + const UP& dragController(); + + private: + UP m_dragStateController = makeUnique(); + }; +} + +inline UP g_layoutManager; \ No newline at end of file diff --git a/src/layout/MasterLayout.cpp b/src/layout/MasterLayout.cpp deleted file mode 100644 index 8c6376ab..00000000 --- a/src/layout/MasterLayout.cpp +++ /dev/null @@ -1,1526 +0,0 @@ -#include "MasterLayout.hpp" -#include "../Compositor.hpp" -#include "../render/decorations/CHyprGroupBarDecoration.hpp" -#include "config/ConfigDataValues.hpp" -#include -#include "../config/ConfigValue.hpp" -#include "../config/ConfigManager.hpp" -#include "../render/Renderer.hpp" -#include "../managers/input/InputManager.hpp" -#include "../managers/LayoutManager.hpp" -#include "../managers/EventManager.hpp" -#include "../desktop/state/FocusState.hpp" -#include "xwayland/XWayland.hpp" - -SMasterNodeData* CHyprMasterLayout::getNodeFromWindow(PHLWINDOW pWindow) { - for (auto& nd : m_masterNodesData) { - if (nd.pWindow.lock() == pWindow) - return &nd; - } - - return nullptr; -} - -int CHyprMasterLayout::getNodesOnWorkspace(const WORKSPACEID& ws) { - int no = 0; - for (auto const& n : m_masterNodesData) { - if (n.workspaceID == ws) - no++; - } - - return no; -} - -int CHyprMasterLayout::getMastersOnWorkspace(const WORKSPACEID& ws) { - int no = 0; - for (auto const& n : m_masterNodesData) { - if (n.workspaceID == ws && n.isMaster) - no++; - } - - return no; -} - -SMasterWorkspaceData* CHyprMasterLayout::getMasterWorkspaceData(const WORKSPACEID& ws) { - for (auto& n : m_masterWorkspacesData) { - if (n.workspaceID == ws) - return &n; - } - - //create on the fly if it doesn't exist yet - const auto PWORKSPACEDATA = &m_masterWorkspacesData.emplace_back(); - PWORKSPACEDATA->workspaceID = ws; - static auto PORIENTATION = CConfigValue("master:orientation"); - - if (*PORIENTATION == "top") - PWORKSPACEDATA->orientation = ORIENTATION_TOP; - else if (*PORIENTATION == "right") - PWORKSPACEDATA->orientation = ORIENTATION_RIGHT; - else if (*PORIENTATION == "bottom") - PWORKSPACEDATA->orientation = ORIENTATION_BOTTOM; - else if (*PORIENTATION == "center") - PWORKSPACEDATA->orientation = ORIENTATION_CENTER; - else - PWORKSPACEDATA->orientation = ORIENTATION_LEFT; - - return PWORKSPACEDATA; -} - -std::string CHyprMasterLayout::getLayoutName() { - return "Master"; -} - -SMasterNodeData* CHyprMasterLayout::getMasterNodeOnWorkspace(const WORKSPACEID& ws) { - for (auto& n : m_masterNodesData) { - if (n.isMaster && n.workspaceID == ws) - return &n; - } - - return nullptr; -} - -void CHyprMasterLayout::onWindowCreatedTiling(PHLWINDOW pWindow, eDirection direction) { - if (pWindow->m_isFloating) - return; - - static auto PNEWONACTIVE = CConfigValue("master:new_on_active"); - static auto PNEWONTOP = CConfigValue("master:new_on_top"); - static auto PNEWSTATUS = CConfigValue("master:new_status"); - - const auto PMONITOR = pWindow->m_monitor.lock(); - - const bool BNEWBEFOREACTIVE = *PNEWONACTIVE == "before"; - const bool BNEWISMASTER = *PNEWSTATUS == "master"; - - const auto PNODE = [&]() { - if (*PNEWONACTIVE != "none" && !BNEWISMASTER) { - const auto pLastNode = getNodeFromWindow(Desktop::focusState()->window()); - if (pLastNode && !(pLastNode->isMaster && (getMastersOnWorkspace(pWindow->workspaceID()) == 1 || *PNEWSTATUS == "slave"))) { - auto it = std::ranges::find(m_masterNodesData, *pLastNode); - if (!BNEWBEFOREACTIVE) - ++it; - return &(*m_masterNodesData.emplace(it)); - } - } - return *PNEWONTOP ? &m_masterNodesData.emplace_front() : &m_masterNodesData.emplace_back(); - }(); - - PNODE->workspaceID = pWindow->workspaceID(); - PNODE->pWindow = pWindow; - - const auto WINDOWSONWORKSPACE = getNodesOnWorkspace(PNODE->workspaceID); - static auto PMFACT = CConfigValue("master:mfact"); - float lastSplitPercent = *PMFACT; - - auto OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == pWindow->m_workspace ? - getNodeFromWindow(Desktop::focusState()->window()) : - getMasterNodeOnWorkspace(pWindow->workspaceID()); - - const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); - static auto PDROPATCURSOR = CConfigValue("master:drop_at_cursor"); - eOrientation orientation = getDynamicOrientation(pWindow->m_workspace); - const auto NODEIT = std::ranges::find(m_masterNodesData, *PNODE); - - bool forceDropAsMaster = false; - // if dragging window to move, drop it at the cursor position instead of bottom/top of stack - if (*PDROPATCURSOR && g_pInputManager->m_dragMode == MBIND_MOVE) { - if (WINDOWSONWORKSPACE > 2) { - for (auto it = m_masterNodesData.begin(); it != m_masterNodesData.end(); ++it) { - if (it->workspaceID != pWindow->workspaceID()) - continue; - const CBox box = it->pWindow->getWindowIdealBoundingBoxIgnoreReserved(); - if (box.containsPoint(MOUSECOORDS)) { - switch (orientation) { - case ORIENTATION_LEFT: - case ORIENTATION_RIGHT: - if (MOUSECOORDS.y > it->pWindow->middle().y) - ++it; - break; - case ORIENTATION_TOP: - case ORIENTATION_BOTTOM: - if (MOUSECOORDS.x > it->pWindow->middle().x) - ++it; - break; - case ORIENTATION_CENTER: break; - default: UNREACHABLE(); - } - m_masterNodesData.splice(it, m_masterNodesData, NODEIT); - break; - } - } - } else if (WINDOWSONWORKSPACE == 2) { - // when dropping as the second tiled window in the workspace, - // make it the master only if the cursor is on the master side of the screen - for (auto const& nd : m_masterNodesData) { - if (nd.isMaster && nd.workspaceID == PNODE->workspaceID) { - switch (orientation) { - case ORIENTATION_LEFT: - case ORIENTATION_CENTER: - if (MOUSECOORDS.x < nd.pWindow->middle().x) - forceDropAsMaster = true; - break; - case ORIENTATION_RIGHT: - if (MOUSECOORDS.x > nd.pWindow->middle().x) - forceDropAsMaster = true; - break; - case ORIENTATION_TOP: - if (MOUSECOORDS.y < nd.pWindow->middle().y) - forceDropAsMaster = true; - break; - case ORIENTATION_BOTTOM: - if (MOUSECOORDS.y > nd.pWindow->middle().y) - forceDropAsMaster = true; - break; - default: UNREACHABLE(); - } - break; - } - } - } - } - - if ((BNEWISMASTER && g_pInputManager->m_dragMode != MBIND_MOVE) // - || WINDOWSONWORKSPACE == 1 // - || (WINDOWSONWORKSPACE > 2 && !pWindow->m_firstMap && OPENINGON && OPENINGON->isMaster) // - || forceDropAsMaster // - || (*PNEWSTATUS == "inherit" && OPENINGON && OPENINGON->isMaster && g_pInputManager->m_dragMode != MBIND_MOVE)) { - - if (BNEWBEFOREACTIVE) { - for (auto& nd : m_masterNodesData | std::views::reverse) { - if (nd.isMaster && nd.workspaceID == PNODE->workspaceID) { - nd.isMaster = false; - lastSplitPercent = nd.percMaster; - break; - } - } - } else { - for (auto& nd : m_masterNodesData) { - if (nd.isMaster && nd.workspaceID == PNODE->workspaceID) { - nd.isMaster = false; - lastSplitPercent = nd.percMaster; - break; - } - } - } - - PNODE->isMaster = true; - PNODE->percMaster = lastSplitPercent; - - // first, check if it isn't too big. - if (const auto MAXSIZE = pWindow->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PMONITOR->m_size.x * lastSplitPercent || MAXSIZE.y < PMONITOR->m_size.y) { - // we can't continue. make it floating. - pWindow->m_isFloating = true; - m_masterNodesData.remove(*PNODE); - g_pLayoutManager->getCurrentLayout()->onWindowCreatedFloating(pWindow); - return; - } - } else { - PNODE->isMaster = false; - PNODE->percMaster = lastSplitPercent; - - // first, check if it isn't too big. - if (const auto MAXSIZE = pWindow->maxSize().value_or(Math::VECTOR2D_MAX); - MAXSIZE.x < PMONITOR->m_size.x * (1 - lastSplitPercent) || MAXSIZE.y < PMONITOR->m_size.y * (1.f / (WINDOWSONWORKSPACE - 1))) { - // we can't continue. make it floating. - pWindow->m_isFloating = true; - m_masterNodesData.remove(*PNODE); - g_pLayoutManager->getCurrentLayout()->onWindowCreatedFloating(pWindow); - return; - } - } - - // recalc - recalculateMonitor(pWindow->monitorID()); - pWindow->m_workspace->updateWindows(); -} - -void CHyprMasterLayout::onWindowRemovedTiling(PHLWINDOW pWindow) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE) - return; - - const auto WORKSPACEID = PNODE->workspaceID; - const auto MASTERSLEFT = getMastersOnWorkspace(WORKSPACEID); - static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); - - pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - pWindow->updateWindowData(); - - if (pWindow->isFullscreen()) - g_pCompositor->setWindowFullscreenInternal(pWindow, FSMODE_NONE); - - if (PNODE->isMaster && (MASTERSLEFT <= 1 || *SMALLSPLIT == 1)) { - // find a new master from top of the list - for (auto& nd : m_masterNodesData) { - if (!nd.isMaster && nd.workspaceID == WORKSPACEID) { - nd.isMaster = true; - nd.percMaster = PNODE->percMaster; - break; - } - } - } - - m_masterNodesData.remove(*PNODE); - - if (getMastersOnWorkspace(WORKSPACEID) == getNodesOnWorkspace(WORKSPACEID) && MASTERSLEFT > 1) { - for (auto& nd : m_masterNodesData | std::views::reverse) { - if (nd.workspaceID == WORKSPACEID) { - nd.isMaster = false; - break; - } - } - } - // BUGFIX: correct bug where closing one master in a stack of 2 would leave - // the screen half bare, and make it difficult to select remaining window - if (getNodesOnWorkspace(WORKSPACEID) == 1) { - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID == WORKSPACEID && !nd.isMaster) { - nd.isMaster = true; - break; - } - } - } - recalculateMonitor(pWindow->monitorID()); - pWindow->m_workspace->updateWindows(); -} - -void CHyprMasterLayout::recalculateMonitor(const MONITORID& monid) { - const auto PMONITOR = g_pCompositor->getMonitorFromID(monid); - - if (!PMONITOR || !PMONITOR->m_activeWorkspace) - return; - - g_pHyprRenderer->damageMonitor(PMONITOR); - - if (PMONITOR->m_activeSpecialWorkspace) - calculateWorkspace(PMONITOR->m_activeSpecialWorkspace); - - calculateWorkspace(PMONITOR->m_activeWorkspace); - -#ifndef NO_XWAYLAND - CBox box = g_pCompositor->calculateX11WorkArea(); - if (!g_pXWayland || !g_pXWayland->m_wm) - return; - g_pXWayland->m_wm->updateWorkArea(box.x, box.y, box.w, box.h); -#endif -} - -void CHyprMasterLayout::calculateWorkspace(PHLWORKSPACE pWorkspace) { - const auto PMONITOR = pWorkspace->m_monitor.lock(); - - if (!PMONITOR) - return; - - if (pWorkspace->m_hasFullscreenWindow) { - // massive hack from the fullscreen func - const auto PFULLWINDOW = pWorkspace->getFullscreenWindow(); - - if (pWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN) { - *PFULLWINDOW->m_realPosition = PMONITOR->m_position; - *PFULLWINDOW->m_realSize = PMONITOR->m_size; - } else if (pWorkspace->m_fullscreenMode == FSMODE_MAXIMIZED) { - SMasterNodeData fakeNode; - fakeNode.pWindow = PFULLWINDOW; - const auto WORKAREA = workAreaOnWorkspace(pWorkspace); - fakeNode.position = WORKAREA.pos(); - fakeNode.size = WORKAREA.size(); - fakeNode.workspaceID = pWorkspace->m_id; - PFULLWINDOW->m_position = fakeNode.position; - PFULLWINDOW->m_size = fakeNode.size; - fakeNode.ignoreFullscreenChecks = true; - - applyNodeDataToWindow(&fakeNode); - } - - // if has fullscreen, don't calculate the rest - return; - } - - const auto PMASTERNODE = getMasterNodeOnWorkspace(pWorkspace->m_id); - - if (!PMASTERNODE) - return; - - eOrientation orientation = getDynamicOrientation(pWorkspace); - bool centerMasterWindow = false; - static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); - static auto CMFALLBACK = CConfigValue("master:center_master_fallback"); - static auto PIGNORERESERVED = CConfigValue("master:center_ignores_reserved"); - static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); - - const auto MASTERS = getMastersOnWorkspace(pWorkspace->m_id); - const auto WINDOWS = getNodesOnWorkspace(pWorkspace->m_id); - const auto STACKWINDOWS = WINDOWS - MASTERS; - const auto WORKAREA = workAreaOnWorkspace(pWorkspace); - const auto UNRESERVED_WIDTH = WORKAREA.width + PMONITOR->m_reservedArea.left() + PMONITOR->m_reservedArea.right(); - - if (orientation == ORIENTATION_CENTER) { - if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) - centerMasterWindow = true; - else { - if (*CMFALLBACK == "left") - orientation = ORIENTATION_LEFT; - else if (*CMFALLBACK == "right") - orientation = ORIENTATION_RIGHT; - else if (*CMFALLBACK == "top") - orientation = ORIENTATION_TOP; - else if (*CMFALLBACK == "bottom") - orientation = ORIENTATION_BOTTOM; - else - orientation = ORIENTATION_LEFT; - } - } - - const float totalSize = (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) ? WORKAREA.w : WORKAREA.h; - const float masterAverageSize = totalSize / MASTERS; - const float slaveAverageSize = totalSize / STACKWINDOWS; - float masterAccumulatedSize = 0; - float slaveAccumulatedSize = 0; - - if (*PSMARTRESIZING) { - // check the total width and height so that later - // if larger/smaller than screen size them down/up - for (auto const& nd : m_masterNodesData) { - if (nd.workspaceID == pWorkspace->m_id) { - if (nd.isMaster) - masterAccumulatedSize += totalSize / MASTERS * nd.percSize; - else - slaveAccumulatedSize += totalSize / STACKWINDOWS * nd.percSize; - } - } - } - - // compute placement of master window(s) - if (WINDOWS == 1 && !centerMasterWindow) { - static auto PALWAYSKEEPPOSITION = CConfigValue("master:always_keep_position"); - if (*PALWAYSKEEPPOSITION) { - const float WIDTH = WORKAREA.w * PMASTERNODE->percMaster; - float nextX = 0; - - if (orientation == ORIENTATION_RIGHT) - nextX = WORKAREA.w - WIDTH; - else if (orientation == ORIENTATION_CENTER) - nextX = (WORKAREA.w - WIDTH) / 2; - - PMASTERNODE->size = Vector2D(WIDTH, WORKAREA.h); - PMASTERNODE->position = WORKAREA.pos() + Vector2D(nextX, 0.0); - } else { - PMASTERNODE->size = WORKAREA.size(); - PMASTERNODE->position = WORKAREA.pos(); - } - - applyNodeDataToWindow(PMASTERNODE); - return; - } else if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) { - const float HEIGHT = STACKWINDOWS != 0 ? WORKAREA.h * PMASTERNODE->percMaster : WORKAREA.h; - float widthLeft = WORKAREA.w; - int mastersLeft = MASTERS; - float nextX = 0; - float nextY = 0; - - if (orientation == ORIENTATION_BOTTOM) - nextY = WORKAREA.h - HEIGHT; - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || !nd.isMaster) - continue; - - float WIDTH = mastersLeft > 1 ? widthLeft / mastersLeft * nd.percSize : widthLeft; - if (WIDTH > widthLeft * 0.9f && mastersLeft > 1) - WIDTH = widthLeft * 0.9f; - - if (*PSMARTRESIZING) { - nd.percSize *= WORKAREA.w / masterAccumulatedSize; - WIDTH = masterAverageSize * nd.percSize; - } - - nd.size = Vector2D(WIDTH, HEIGHT); - nd.position = WORKAREA.pos() + Vector2D(nextX, nextY); - applyNodeDataToWindow(&nd); - - mastersLeft--; - widthLeft -= WIDTH; - nextX += WIDTH; - } - } else { // orientation left, right or center - const float TOTAL_WIDTH = *PIGNORERESERVED && centerMasterWindow ? UNRESERVED_WIDTH : WORKAREA.w; - float WIDTH = TOTAL_WIDTH; - float heightLeft = WORKAREA.h; - int mastersLeft = MASTERS; - float nextX = 0; - float nextY = 0; - - if (STACKWINDOWS > 0 || centerMasterWindow) - WIDTH *= PMASTERNODE->percMaster; - - if (orientation == ORIENTATION_RIGHT) - nextX = WORKAREA.w - WIDTH; - else if (centerMasterWindow) - nextX += (TOTAL_WIDTH - WIDTH) / 2; - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || !nd.isMaster) - continue; - - float HEIGHT = mastersLeft > 1 ? heightLeft / mastersLeft * nd.percSize : heightLeft; - if (HEIGHT > heightLeft * 0.9f && mastersLeft > 1) - HEIGHT = heightLeft * 0.9f; - - if (*PSMARTRESIZING) { - nd.percSize *= WORKAREA.h / masterAccumulatedSize; - HEIGHT = masterAverageSize * nd.percSize; - } - - nd.size = Vector2D(WIDTH, HEIGHT); - nd.position = (*PIGNORERESERVED && centerMasterWindow ? WORKAREA.pos() - Vector2D(PMONITOR->m_reservedArea.left(), 0.0) : WORKAREA.pos()) + Vector2D(nextX, nextY); - applyNodeDataToWindow(&nd); - - mastersLeft--; - heightLeft -= HEIGHT; - nextY += HEIGHT; - } - } - - if (STACKWINDOWS == 0) - return; - - // compute placement of slave window(s) - int slavesLeft = STACKWINDOWS; - if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) { - const float HEIGHT = WORKAREA.h - PMASTERNODE->size.y; - float widthLeft = WORKAREA.w; - float nextX = 0; - float nextY = 0; - - if (orientation == ORIENTATION_TOP) - nextY = PMASTERNODE->size.y; - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || nd.isMaster) - continue; - - float WIDTH = slavesLeft > 1 ? widthLeft / slavesLeft * nd.percSize : widthLeft; - if (WIDTH > widthLeft * 0.9f && slavesLeft > 1) - WIDTH = widthLeft * 0.9f; - - if (*PSMARTRESIZING) { - nd.percSize *= WORKAREA.w / slaveAccumulatedSize; - WIDTH = slaveAverageSize * nd.percSize; - } - - nd.size = Vector2D(WIDTH, HEIGHT); - nd.position = WORKAREA.pos() + Vector2D(nextX, nextY); - applyNodeDataToWindow(&nd); - - slavesLeft--; - widthLeft -= WIDTH; - nextX += WIDTH; - } - } else if (orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT) { - const float WIDTH = WORKAREA.w - PMASTERNODE->size.x; - float heightLeft = WORKAREA.h; - float nextY = 0; - float nextX = 0; - - if (orientation == ORIENTATION_LEFT) - nextX = PMASTERNODE->size.x; - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || nd.isMaster) - continue; - - float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd.percSize : heightLeft; - if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1) - HEIGHT = heightLeft * 0.9f; - - if (*PSMARTRESIZING) { - nd.percSize *= WORKAREA.h / slaveAccumulatedSize; - HEIGHT = slaveAverageSize * nd.percSize; - } - - nd.size = Vector2D(WIDTH, HEIGHT); - nd.position = WORKAREA.pos() + Vector2D(nextX, nextY); - applyNodeDataToWindow(&nd); - - slavesLeft--; - heightLeft -= HEIGHT; - nextY += HEIGHT; - } - } else { // slaves for centered master window(s) - const float WIDTH = ((*PIGNORERESERVED ? UNRESERVED_WIDTH : WORKAREA.w) - PMASTERNODE->size.x) / 2.0; - float heightLeft = 0; - float heightLeftL = WORKAREA.h; - float heightLeftR = WORKAREA.h; - float nextX = 0; - float nextY = 0; - float nextYL = 0; - float nextYR = 0; - bool onRight = *CMFALLBACK == "right"; - int slavesLeftL = 1 + (slavesLeft - 1) / 2; - int slavesLeftR = slavesLeft - slavesLeftL; - - if (onRight) { - slavesLeftR = 1 + (slavesLeft - 1) / 2; - slavesLeftL = slavesLeft - slavesLeftR; - } - - const float slaveAverageHeightL = WORKAREA.h / slavesLeftL; - const float slaveAverageHeightR = WORKAREA.h / slavesLeftR; - float slaveAccumulatedHeightL = 0; - float slaveAccumulatedHeightR = 0; - - if (*PSMARTRESIZING) { - for (auto const& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || nd.isMaster) - continue; - - if (onRight) { - slaveAccumulatedHeightR += slaveAverageHeightR * nd.percSize; - } else { - slaveAccumulatedHeightL += slaveAverageHeightL * nd.percSize; - } - onRight = !onRight; - } - - onRight = *CMFALLBACK == "right"; - } - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID != pWorkspace->m_id || nd.isMaster) - continue; - - if (onRight) { - nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? PMONITOR->m_reservedArea.left() : 0); - nextY = nextYR; - heightLeft = heightLeftR; - slavesLeft = slavesLeftR; - } else { - nextX = 0; - nextY = nextYL; - heightLeft = heightLeftL; - slavesLeft = slavesLeftL; - } - - float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd.percSize : heightLeft; - if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1) - HEIGHT = heightLeft * 0.9f; - - if (*PSMARTRESIZING) { - if (onRight) { - nd.percSize *= WORKAREA.h / slaveAccumulatedHeightR; - HEIGHT = slaveAverageHeightR * nd.percSize; - } else { - nd.percSize *= WORKAREA.h / slaveAccumulatedHeightL; - HEIGHT = slaveAverageHeightL * nd.percSize; - } - } - - nd.size = Vector2D(*PIGNORERESERVED ? (WIDTH - (onRight ? PMONITOR->m_reservedArea.right() : PMONITOR->m_reservedArea.left())) : WIDTH, HEIGHT); - nd.position = WORKAREA.pos() + Vector2D(nextX, nextY); - applyNodeDataToWindow(&nd); - - if (onRight) { - heightLeftR -= HEIGHT; - nextYR += HEIGHT; - slavesLeftR--; - } else { - heightLeftL -= HEIGHT; - nextYL += HEIGHT; - slavesLeftL--; - } - - onRight = !onRight; - } - } -} - -void CHyprMasterLayout::applyNodeDataToWindow(SMasterNodeData* pNode) { - PHLMONITOR PMONITOR = nullptr; - - const auto WS = g_pCompositor->getWorkspaceByID(pNode->workspaceID); - - if (g_pCompositor->isWorkspaceSpecial(pNode->workspaceID)) { - for (auto const& m : g_pCompositor->m_monitors) { - if (m->activeSpecialWorkspaceID() == pNode->workspaceID) { - PMONITOR = m; - break; - } - } - } else if (WS) - PMONITOR = WS->m_monitor.lock(); - - if (!PMONITOR || !WS) { - Log::logger->log(Log::ERR, "Orphaned Node {}!!", pNode); - return; - } - - // for gaps outer - const auto WORKAREA = workAreaOnWorkspace(WS); - const bool DISPLAYLEFT = STICKS(pNode->position.x, WORKAREA.x); - const bool DISPLAYRIGHT = STICKS(pNode->position.x + pNode->size.x, WORKAREA.x + WORKAREA.w); - const bool DISPLAYTOP = STICKS(pNode->position.y, WORKAREA.y); - const bool DISPLAYBOTTOM = STICKS(pNode->position.y + pNode->size.y, WORKAREA.y + WORKAREA.h); - - const auto PWINDOW = pNode->pWindow.lock(); - // get specific gaps and rules for this workspace, - // if user specified them in config - const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(PWINDOW->m_workspace); - - if (PWINDOW->isFullscreen() && !pNode->ignoreFullscreenChecks) - return; - - PWINDOW->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - PWINDOW->updateWindowData(); - - static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); - static auto PGAPSINDATA = CConfigValue("general:gaps_in"); - auto* PGAPSIN = sc((PGAPSINDATA.ptr())->getData()); - - auto gapsIn = WORKSPACERULE.gapsIn.value_or(*PGAPSIN); - - if (!validMapped(PWINDOW)) { - Log::logger->log(Log::ERR, "Node {} holding invalid {}!!", pNode, PWINDOW); - return; - } - - PWINDOW->m_size = pNode->size; - PWINDOW->m_position = pNode->position; - - PWINDOW->updateWindowDecos(); - - auto calcPos = PWINDOW->m_position; - auto calcSize = PWINDOW->m_size; - - const auto OFFSETTOPLEFT = Vector2D(sc(DISPLAYLEFT ? 0 : gapsIn.m_left), sc(DISPLAYTOP ? 0 : gapsIn.m_top)); - - const auto OFFSETBOTTOMRIGHT = Vector2D(sc(DISPLAYRIGHT ? 0 : gapsIn.m_right), sc(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom)); - - calcPos = calcPos + OFFSETTOPLEFT; - calcSize = calcSize - OFFSETTOPLEFT - OFFSETBOTTOMRIGHT; - - const auto RESERVED = PWINDOW->getFullWindowReservedArea(); - calcPos = calcPos + RESERVED.topLeft; - calcSize = calcSize - (RESERVED.topLeft + RESERVED.bottomRight); - - Vector2D availableSpace = calcSize; - - static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); - - if (*PCLAMP_TILED) { - const auto borderSize = PWINDOW->getRealBorderSize(); - Vector2D monitorAvailable = WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize}; - - Vector2D minSize = PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}).clamp(Vector2D{0, 0}, monitorAvailable); - Vector2D maxSize = PWINDOW->isFullscreen() ? Vector2D{INFINITY, INFINITY} : - PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}).clamp(Vector2D{0, 0}, monitorAvailable); - calcSize = calcSize.clamp(minSize, maxSize); - - calcPos += (availableSpace - calcSize) / 2.0; - - calcPos.x = std::clamp(calcPos.x, WORKAREA.x + borderSize, WORKAREA.x + WORKAREA.w - calcSize.x - borderSize); - calcPos.y = std::clamp(calcPos.y, WORKAREA.y + borderSize, WORKAREA.y + WORKAREA.h - calcSize.y - borderSize); - } - - if (PWINDOW->onSpecialWorkspace() && !PWINDOW->isFullscreen()) { - static auto PSCALEFACTOR = CConfigValue("master:special_scale_factor"); - - CBox wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR}; - wb.round(); // avoid rounding mess - - *PWINDOW->m_realPosition = wb.pos(); - *PWINDOW->m_realSize = wb.size(); - } else { - CBox wb = {calcPos, calcSize}; - wb.round(); // avoid rounding mess - - *PWINDOW->m_realPosition = wb.pos(); - *PWINDOW->m_realSize = wb.size(); - } - - if (m_forceWarps && !*PANIMATE) { - g_pHyprRenderer->damageWindow(PWINDOW); - - PWINDOW->m_realPosition->warp(); - PWINDOW->m_realSize->warp(); - - g_pHyprRenderer->damageWindow(PWINDOW); - } - - PWINDOW->updateWindowDecos(); -} - -bool CHyprMasterLayout::isWindowTiled(PHLWINDOW pWindow) { - return getNodeFromWindow(pWindow) != nullptr; -} - -void CHyprMasterLayout::resizeActiveWindow(const Vector2D& pixResize, eRectCorner corner, PHLWINDOW pWindow) { - const auto PWINDOW = pWindow ? pWindow : Desktop::focusState()->window(); - - if (!validMapped(PWINDOW)) - return; - - const auto PNODE = getNodeFromWindow(PWINDOW); - - if (!PNODE) { - *PWINDOW->m_realSize = (PWINDOW->m_realSize->goal() + pixResize) - .clamp(PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}), - PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY})); - PWINDOW->updateWindowDecos(); - return; - } - - const auto PMONITOR = PWINDOW->m_monitor.lock(); - static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); - static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); - - const auto WORKAREA = PMONITOR->logicalBoxMinusReserved(); - const bool DISPLAYBOTTOM = STICKS(PWINDOW->m_position.y + PWINDOW->m_size.y, WORKAREA.y + WORKAREA.h); - const bool DISPLAYRIGHT = STICKS(PWINDOW->m_position.x + PWINDOW->m_size.x, WORKAREA.x + WORKAREA.w); - const bool DISPLAYTOP = STICKS(PWINDOW->m_position.y, WORKAREA.y); - const bool DISPLAYLEFT = STICKS(PWINDOW->m_position.x, WORKAREA.x); - - const bool LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT; - const bool TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT; - const bool NONE = corner == CORNER_NONE; - - const auto MASTERS = getMastersOnWorkspace(PNODE->workspaceID); - const auto WINDOWS = getNodesOnWorkspace(PNODE->workspaceID); - const auto STACKWINDOWS = WINDOWS - MASTERS; - - eOrientation orientation = getDynamicOrientation(PWINDOW->m_workspace); - bool centered = orientation == ORIENTATION_CENTER && (STACKWINDOWS >= *SLAVECOUNTFORCENTER); - double delta = 0; - - if (getNodesOnWorkspace(PWINDOW->workspaceID()) == 1 && !centered) - return; - - m_forceWarps = true; - - switch (orientation) { - case ORIENTATION_LEFT: delta = pixResize.x / PMONITOR->m_size.x; break; - case ORIENTATION_RIGHT: delta = -pixResize.x / PMONITOR->m_size.x; break; - case ORIENTATION_BOTTOM: delta = -pixResize.y / PMONITOR->m_size.y; break; - case ORIENTATION_TOP: delta = pixResize.y / PMONITOR->m_size.y; break; - case ORIENTATION_CENTER: - delta = pixResize.x / PMONITOR->m_size.x; - if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) { - if (!NONE || !PNODE->isMaster) - delta *= 2; - if ((!PNODE->isMaster && DISPLAYLEFT) || (PNODE->isMaster && LEFT && *PSMARTRESIZING)) - delta = -delta; - } - break; - default: UNREACHABLE(); - } - - const auto workspaceIdForResizing = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->activeSpecialWorkspaceID() : PMONITOR->activeWorkspaceID(); - for (auto& n : m_masterNodesData) { - if (n.isMaster && n.workspaceID == workspaceIdForResizing) - n.percMaster = std::clamp(n.percMaster + delta, 0.05, 0.95); - } - - // check the up/down resize - const bool isStackVertical = orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT || orientation == ORIENTATION_CENTER; - - const auto RESIZEDELTA = isStackVertical ? pixResize.y : pixResize.x; - - auto nodesInSameColumn = PNODE->isMaster ? MASTERS : STACKWINDOWS; - if (orientation == ORIENTATION_CENTER && !PNODE->isMaster) - nodesInSameColumn = DISPLAYRIGHT ? (nodesInSameColumn + 1) / 2 : nodesInSameColumn / 2; - - const auto SIZE = isStackVertical ? WORKAREA.h / nodesInSameColumn : WORKAREA.w / nodesInSameColumn; - - if (RESIZEDELTA != 0 && nodesInSameColumn > 1) { - if (!*PSMARTRESIZING) { - PNODE->percSize = std::clamp(PNODE->percSize + RESIZEDELTA / SIZE, 0.05, 1.95); - } else { - const auto NODEIT = std::ranges::find(m_masterNodesData, *PNODE); - const auto REVNODEIT = std::ranges::find(m_masterNodesData | std::views::reverse, *PNODE); - - const float totalSize = isStackVertical ? WORKAREA.h : WORKAREA.w; - const float minSize = totalSize / nodesInSameColumn * 0.2; - const bool resizePrevNodes = isStackVertical ? (TOP || DISPLAYBOTTOM) && !DISPLAYTOP : (LEFT || DISPLAYRIGHT) && !DISPLAYLEFT; - - int nodesLeft = 0; - float sizeLeft = 0; - int nodeCount = 0; - // check the sizes of all the nodes to be resized for later calculation - auto checkNodesLeft = [&sizeLeft, &nodesLeft, orientation, isStackVertical, &nodeCount, PNODE](auto it) { - if (it.isMaster != PNODE->isMaster || it.workspaceID != PNODE->workspaceID) - return; - nodeCount++; - if (!it.isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1) - return; - sizeLeft += isStackVertical ? it.size.y : it.size.x; - nodesLeft++; - }; - float resizeDiff; - if (resizePrevNodes) { - std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), checkNodesLeft); - resizeDiff = -RESIZEDELTA; - } else { - std::for_each(std::next(NODEIT), m_masterNodesData.end(), checkNodesLeft); - resizeDiff = RESIZEDELTA; - } - - const float nodeSize = isStackVertical ? PNODE->size.y : PNODE->size.x; - const float maxSizeIncrease = sizeLeft - nodesLeft * minSize; - const float maxSizeDecrease = minSize - nodeSize; - - // leaves enough room for the other nodes - resizeDiff = std::clamp(resizeDiff, maxSizeDecrease, maxSizeIncrease); - PNODE->percSize += resizeDiff / SIZE; - - // resize the other nodes - nodeCount = 0; - auto resizeNodesLeft = [maxSizeIncrease, resizeDiff, minSize, orientation, isStackVertical, SIZE, &nodeCount, nodesLeft, PNODE](auto& it) { - if (it.isMaster != PNODE->isMaster || it.workspaceID != PNODE->workspaceID) - return; - nodeCount++; - // if center orientation, only resize when on the same side - if (!it.isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1) - return; - const float size = isStackVertical ? it.size.y : it.size.x; - const float resizeDeltaForEach = maxSizeIncrease != 0 ? resizeDiff * (size - minSize) / maxSizeIncrease : resizeDiff / nodesLeft; - it.percSize -= resizeDeltaForEach / SIZE; - }; - if (resizePrevNodes) { - std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), resizeNodesLeft); - } else { - std::for_each(std::next(NODEIT), m_masterNodesData.end(), resizeNodesLeft); - } - } - } - - recalculateMonitor(PMONITOR->m_id); - - m_forceWarps = false; -} - -void CHyprMasterLayout::fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) { - const auto PMONITOR = pWindow->m_monitor.lock(); - const auto PWORKSPACE = pWindow->m_workspace; - - // save position and size if floating - if (pWindow->m_isFloating && CURRENT_EFFECTIVE_MODE == FSMODE_NONE) { - pWindow->m_lastFloatingSize = pWindow->m_realSize->goal(); - pWindow->m_lastFloatingPosition = pWindow->m_realPosition->goal(); - pWindow->m_position = pWindow->m_realPosition->goal(); - pWindow->m_size = pWindow->m_realSize->goal(); - } - - if (EFFECTIVE_MODE == FSMODE_NONE) { - // if it got its fullscreen disabled, set back its node if it had one - const auto PNODE = getNodeFromWindow(pWindow); - if (PNODE) - applyNodeDataToWindow(PNODE); - else { - // get back its' dimensions from position and size - *pWindow->m_realPosition = pWindow->m_lastFloatingPosition; - *pWindow->m_realSize = pWindow->m_lastFloatingSize; - - pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT); - pWindow->updateWindowData(); - } - } else { - // apply new pos and size being monitors' box - if (EFFECTIVE_MODE == FSMODE_FULLSCREEN) { - *pWindow->m_realPosition = PMONITOR->m_position; - *pWindow->m_realSize = PMONITOR->m_size; - } else { - // This is a massive hack. - // We make a fake "only" node and apply - // To keep consistent with the settings without C+P code - - SMasterNodeData fakeNode; - const auto WORKAREA = PMONITOR->logicalBoxMinusReserved(); - fakeNode.pWindow = pWindow; - fakeNode.position = WORKAREA.pos(); - fakeNode.size = WORKAREA.size(); - fakeNode.workspaceID = pWindow->workspaceID(); - pWindow->m_position = fakeNode.position; - pWindow->m_size = fakeNode.size; - fakeNode.ignoreFullscreenChecks = true; - - applyNodeDataToWindow(&fakeNode); - } - } - - g_pCompositor->changeWindowZOrder(pWindow, true); -} - -void CHyprMasterLayout::recalculateWindow(PHLWINDOW pWindow) { - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE) - return; - - recalculateMonitor(pWindow->monitorID()); -} - -SWindowRenderLayoutHints CHyprMasterLayout::requestRenderHints(PHLWINDOW pWindow) { - // window should be valid, insallah - - SWindowRenderLayoutHints hints; - - return hints; // master doesn't have any hints -} - -void CHyprMasterLayout::moveWindowTo(PHLWINDOW pWindow, const std::string& dir, bool silent) { - if (!isDirection(dir)) - return; - - const auto PWINDOW2 = g_pCompositor->getWindowInDirection(pWindow, dir[0]); - - if (!PWINDOW2) - return; - - pWindow->setAnimationsToMove(); - - if (pWindow->m_workspace != PWINDOW2->m_workspace) { - // if different monitors, send to monitor - onWindowRemovedTiling(pWindow); - pWindow->moveToWorkspace(PWINDOW2->m_workspace); - pWindow->m_monitor = PWINDOW2->m_monitor; - if (!silent) { - const auto pMonitor = pWindow->m_monitor.lock(); - Desktop::focusState()->rawMonitorFocus(pMonitor); - } - onWindowCreatedTiling(pWindow); - } else { - // if same monitor, switch windows - switchWindows(pWindow, PWINDOW2); - if (silent) - Desktop::focusState()->fullWindowFocus(PWINDOW2); - } - - pWindow->updateGroupOutputs(); - if (!pWindow->m_groupData.pNextWindow.expired()) { - PHLWINDOW next = pWindow->m_groupData.pNextWindow.lock(); - while (next != pWindow) { - next->updateToplevel(); - next = next->m_groupData.pNextWindow.lock(); - } - } -} - -void CHyprMasterLayout::switchWindows(PHLWINDOW pWindow, PHLWINDOW pWindow2) { - // windows should be valid, insallah - - const auto PNODE = getNodeFromWindow(pWindow); - const auto PNODE2 = getNodeFromWindow(pWindow2); - - if (!PNODE2 || !PNODE) - return; - - if (PNODE->workspaceID != PNODE2->workspaceID) { - std::swap(pWindow2->m_monitor, pWindow->m_monitor); - std::swap(pWindow2->m_workspace, pWindow->m_workspace); - } - - // massive hack: just swap window pointers, lol - PNODE->pWindow = pWindow2; - PNODE2->pWindow = pWindow; - - pWindow->setAnimationsToMove(); - pWindow2->setAnimationsToMove(); - - recalculateMonitor(pWindow->monitorID()); - if (PNODE2->workspaceID != PNODE->workspaceID) - recalculateMonitor(pWindow2->monitorID()); - - g_pHyprRenderer->damageWindow(pWindow); - g_pHyprRenderer->damageWindow(pWindow2); -} - -void CHyprMasterLayout::alterSplitRatio(PHLWINDOW pWindow, float ratio, bool exact) { - // window should be valid, insallah - - const auto PNODE = getNodeFromWindow(pWindow); - - if (!PNODE) - return; - - const auto PMASTER = getMasterNodeOnWorkspace(pWindow->workspaceID()); - - float newRatio = exact ? ratio : PMASTER->percMaster + ratio; - PMASTER->percMaster = std::clamp(newRatio, 0.05f, 0.95f); - - recalculateMonitor(pWindow->monitorID()); -} - -PHLWINDOW CHyprMasterLayout::getNextWindow(PHLWINDOW pWindow, bool next, bool loop) { - if (!isWindowTiled(pWindow)) - return nullptr; - - const auto PNODE = getNodeFromWindow(pWindow); - - auto nodes = m_masterNodesData; - if (!next) - std::ranges::reverse(nodes); - - const auto NODEIT = std::ranges::find(nodes, *PNODE); - - const bool ISMASTER = PNODE->isMaster; - - auto CANDIDATE = std::find_if(NODEIT, nodes.end(), [&](const auto& other) { return other != *PNODE && ISMASTER == other.isMaster && other.workspaceID == PNODE->workspaceID; }); - if (CANDIDATE == nodes.end()) - CANDIDATE = std::ranges::find_if(nodes, [&](const auto& other) { return other != *PNODE && ISMASTER != other.isMaster && other.workspaceID == PNODE->workspaceID; }); - - if (CANDIDATE != nodes.end() && !loop) { - if (CANDIDATE->isMaster && next) - return nullptr; - if (!CANDIDATE->isMaster && ISMASTER && !next) - return nullptr; - } - - return CANDIDATE == nodes.end() ? nullptr : CANDIDATE->pWindow.lock(); -} - -std::any CHyprMasterLayout::layoutMessage(SLayoutMessageHeader header, std::string message) { - auto switchToWindow = [&](PHLWINDOW PWINDOWTOCHANGETO) { - if (!validMapped(PWINDOWTOCHANGETO)) - return; - - Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO); - g_pCompositor->warpCursorTo(PWINDOWTOCHANGETO->middle()); - - g_pInputManager->m_forcedFocus = PWINDOWTOCHANGETO; - g_pInputManager->simulateMouseMovement(); - g_pInputManager->m_forcedFocus.reset(); - }; - - CVarList vars(message, 0, ' '); - - if (vars.size() < 1 || vars[0].empty()) { - Log::logger->log(Log::ERR, "layoutmsg called without params"); - return 0; - } - - auto command = vars[0]; - - // swapwithmaster - // first message argument can have the following values: - // * master - keep the focus at the new master - // * child - keep the focus at the new child - // * auto (default) - swap the focus (keep the focus of the previously selected window) - // * ignoremaster - ignore if master is focused - if (command == "swapwithmaster") { - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return 0; - - if (!isWindowTiled(PWINDOW)) - return 0; - - const auto PMASTER = getMasterNodeOnWorkspace(PWINDOW->workspaceID()); - - if (!PMASTER) - return 0; - - const auto NEWCHILD = PMASTER->pWindow.lock(); - - const bool IGNORE_IF_MASTER = vars.size() >= 2 && std::ranges::any_of(vars, [](const auto& e) { return e == "ignoremaster"; }); - - if (PMASTER->pWindow.lock() != PWINDOW) { - const auto& NEWMASTER = PWINDOW; - const bool newFocusToChild = vars.size() >= 2 && vars[1] == "child"; - switchWindows(NEWMASTER, NEWCHILD); - const auto NEWFOCUS = newFocusToChild ? NEWCHILD : NEWMASTER; - switchToWindow(NEWFOCUS); - } else if (!IGNORE_IF_MASTER) { - for (auto const& n : m_masterNodesData) { - if (n.workspaceID == PMASTER->workspaceID && !n.isMaster) { - const auto NEWMASTER = n.pWindow.lock(); - switchWindows(NEWMASTER, NEWCHILD); - const bool newFocusToMaster = vars.size() >= 2 && vars[1] == "master"; - const auto NEWFOCUS = newFocusToMaster ? NEWMASTER : NEWCHILD; - switchToWindow(NEWFOCUS); - break; - } - } - } - - return 0; - } - // focusmaster - // first message argument can have the following values: - // * master - keep the focus at the new master, even if it was focused before - // * previous - focus window which was previously switched from using `focusmaster previous` command, otherwise fallback to `auto` - // * auto (default) - swap the focus with the first child, if the current focus was master, otherwise focus master - else if (command == "focusmaster") { - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return 0; - - const auto PMASTER = getMasterNodeOnWorkspace(PWINDOW->workspaceID()); - - if (!PMASTER) - return 0; - - const auto& ARG = vars[1]; // returns empty string if out of bounds - - if (PMASTER->pWindow.lock() != PWINDOW) { - switchToWindow(PMASTER->pWindow.lock()); - // save previously focused window (only for `previous` mode) - if (ARG == "previous") - getMasterWorkspaceData(PWINDOW->workspaceID())->focusMasterPrev = PWINDOW; - return 0; - } - - const auto focusAuto = [&]() { - // focus first non-master window - for (auto const& n : m_masterNodesData) { - if (n.workspaceID == PMASTER->workspaceID && !n.isMaster) { - switchToWindow(n.pWindow.lock()); - break; - } - } - }; - - if (ARG == "master") - return 0; - // switch to previously saved window - else if (ARG == "previous") { - const auto PREVWINDOW = getMasterWorkspaceData(PWINDOW->workspaceID())->focusMasterPrev.lock(); - const bool VALID = validMapped(PREVWINDOW) && PWINDOW->workspaceID() == PREVWINDOW->workspaceID() && PWINDOW != PREVWINDOW; - VALID ? switchToWindow(PREVWINDOW) : focusAuto(); - } else - focusAuto(); - } else if (command == "cyclenext") { - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return 0; - - const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; - const auto PNEXTWINDOW = getNextWindow(PWINDOW, true, !NOLOOP); - switchToWindow(PNEXTWINDOW); - } else if (command == "cycleprev") { - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return 0; - - const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; - const auto PPREVWINDOW = getNextWindow(PWINDOW, false, !NOLOOP); - switchToWindow(PPREVWINDOW); - } else if (command == "swapnext") { - if (!validMapped(header.pWindow)) - return 0; - - if (header.pWindow->m_isFloating) { - g_pKeybindManager->m_dispatchers["swapnext"](""); - return 0; - } - - const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; - const auto PWINDOWTOSWAPWITH = getNextWindow(header.pWindow, true, !NOLOOP); - - if (PWINDOWTOSWAPWITH) { - g_pCompositor->setWindowFullscreenInternal(header.pWindow, FSMODE_NONE); - switchWindows(header.pWindow, PWINDOWTOSWAPWITH); - switchToWindow(header.pWindow); - } - } else if (command == "swapprev") { - if (!validMapped(header.pWindow)) - return 0; - - if (header.pWindow->m_isFloating) { - g_pKeybindManager->m_dispatchers["swapnext"]("prev"); - return 0; - } - - const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; - const auto PWINDOWTOSWAPWITH = getNextWindow(header.pWindow, false, !NOLOOP); - - if (PWINDOWTOSWAPWITH) { - g_pCompositor->setWindowFullscreenClient(header.pWindow, FSMODE_NONE); - switchWindows(header.pWindow, PWINDOWTOSWAPWITH); - switchToWindow(header.pWindow); - } - } else if (command == "addmaster") { - if (!validMapped(header.pWindow)) - return 0; - - if (header.pWindow->m_isFloating) - return 0; - - const auto PNODE = getNodeFromWindow(header.pWindow); - - const auto WINDOWS = getNodesOnWorkspace(header.pWindow->workspaceID()); - const auto MASTERS = getMastersOnWorkspace(header.pWindow->workspaceID()); - static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); - - if (MASTERS + 2 > WINDOWS && *SMALLSPLIT == 0) - return 0; - - g_pCompositor->setWindowFullscreenInternal(header.pWindow, FSMODE_NONE); - - if (!PNODE || PNODE->isMaster) { - // first non-master node - for (auto& n : m_masterNodesData) { - if (n.workspaceID == header.pWindow->workspaceID() && !n.isMaster) { - n.isMaster = true; - break; - } - } - } else { - PNODE->isMaster = true; - } - - recalculateMonitor(header.pWindow->monitorID()); - - } else if (command == "removemaster") { - - if (!validMapped(header.pWindow)) - return 0; - - if (header.pWindow->m_isFloating) - return 0; - - const auto PNODE = getNodeFromWindow(header.pWindow); - - const auto WINDOWS = getNodesOnWorkspace(header.pWindow->workspaceID()); - const auto MASTERS = getMastersOnWorkspace(header.pWindow->workspaceID()); - - if (WINDOWS < 2 || MASTERS < 2) - return 0; - - g_pCompositor->setWindowFullscreenInternal(header.pWindow, FSMODE_NONE); - - if (!PNODE || !PNODE->isMaster) { - // first non-master node - for (auto& nd : m_masterNodesData | std::views::reverse) { - if (nd.workspaceID == header.pWindow->workspaceID() && nd.isMaster) { - nd.isMaster = false; - break; - } - } - } else { - PNODE->isMaster = false; - } - - recalculateMonitor(header.pWindow->monitorID()); - } else if (command == "orientationleft" || command == "orientationright" || command == "orientationtop" || command == "orientationbottom" || command == "orientationcenter") { - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return 0; - - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - - const auto PWORKSPACEDATA = getMasterWorkspaceData(PWINDOW->workspaceID()); - - if (command == "orientationleft") - PWORKSPACEDATA->orientation = ORIENTATION_LEFT; - else if (command == "orientationright") - PWORKSPACEDATA->orientation = ORIENTATION_RIGHT; - else if (command == "orientationtop") - PWORKSPACEDATA->orientation = ORIENTATION_TOP; - else if (command == "orientationbottom") - PWORKSPACEDATA->orientation = ORIENTATION_BOTTOM; - else if (command == "orientationcenter") - PWORKSPACEDATA->orientation = ORIENTATION_CENTER; - - recalculateMonitor(header.pWindow->monitorID()); - - } else if (command == "orientationnext") { - runOrientationCycle(header, nullptr, 1); - } else if (command == "orientationprev") { - runOrientationCycle(header, nullptr, -1); - } else if (command == "orientationcycle") { - runOrientationCycle(header, &vars, 1); - } else if (command == "mfact") { - g_pKeybindManager->m_dispatchers["splitratio"](vars[1] + " " + vars[2]); - } else if (command == "rollnext") { - const auto PWINDOW = header.pWindow; - const auto PNODE = getNodeFromWindow(PWINDOW); - - if (!PNODE) - return 0; - - const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNodeOnWorkspace(PNODE->workspaceID); - if (!OLDMASTER) - return 0; - - const auto OLDMASTERIT = std::ranges::find(m_masterNodesData, *OLDMASTER); - - for (auto& nd : m_masterNodesData) { - if (nd.workspaceID == PNODE->workspaceID && !nd.isMaster) { - nd.isMaster = true; - const auto NEWMASTERIT = std::ranges::find(m_masterNodesData, nd); - m_masterNodesData.splice(OLDMASTERIT, m_masterNodesData, NEWMASTERIT); - switchToWindow(nd.pWindow.lock()); - OLDMASTER->isMaster = false; - m_masterNodesData.splice(m_masterNodesData.end(), m_masterNodesData, OLDMASTERIT); - break; - } - } - - recalculateMonitor(PWINDOW->monitorID()); - } else if (command == "rollprev") { - const auto PWINDOW = header.pWindow; - const auto PNODE = getNodeFromWindow(PWINDOW); - - if (!PNODE) - return 0; - - const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNodeOnWorkspace(PNODE->workspaceID); - if (!OLDMASTER) - return 0; - - const auto OLDMASTERIT = std::ranges::find(m_masterNodesData, *OLDMASTER); - - for (auto& nd : m_masterNodesData | std::views::reverse) { - if (nd.workspaceID == PNODE->workspaceID && !nd.isMaster) { - nd.isMaster = true; - const auto NEWMASTERIT = std::ranges::find(m_masterNodesData, nd); - m_masterNodesData.splice(OLDMASTERIT, m_masterNodesData, NEWMASTERIT); - switchToWindow(nd.pWindow.lock()); - OLDMASTER->isMaster = false; - m_masterNodesData.splice(m_masterNodesData.begin(), m_masterNodesData, OLDMASTERIT); - break; - } - } - - recalculateMonitor(PWINDOW->monitorID()); - } - - return 0; -} - -// If vars is null, we use the default list -void CHyprMasterLayout::runOrientationCycle(SLayoutMessageHeader& header, CVarList* vars, int direction) { - std::vector cycle; - if (vars != nullptr) - buildOrientationCycleVectorFromVars(cycle, *vars); - - if (cycle.empty()) - buildOrientationCycleVectorFromEOperation(cycle); - - const auto PWINDOW = header.pWindow; - - if (!PWINDOW) - return; - - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - - const auto PWORKSPACEDATA = getMasterWorkspaceData(PWINDOW->workspaceID()); - - int nextOrPrev = 0; - for (size_t i = 0; i < cycle.size(); ++i) { - if (PWORKSPACEDATA->orientation == cycle[i]) { - nextOrPrev = i + direction; - break; - } - } - - if (nextOrPrev >= sc(cycle.size())) - nextOrPrev = nextOrPrev % sc(cycle.size()); - else if (nextOrPrev < 0) - nextOrPrev = cycle.size() + (nextOrPrev % sc(cycle.size())); - - PWORKSPACEDATA->orientation = cycle.at(nextOrPrev); - recalculateMonitor(header.pWindow->monitorID()); -} - -void CHyprMasterLayout::buildOrientationCycleVectorFromEOperation(std::vector& cycle) { - for (int i = 0; i <= ORIENTATION_CENTER; ++i) { - cycle.push_back(sc(i)); - } -} - -void CHyprMasterLayout::buildOrientationCycleVectorFromVars(std::vector& cycle, CVarList& vars) { - for (size_t i = 1; i < vars.size(); ++i) { - if (vars[i] == "top") { - cycle.push_back(ORIENTATION_TOP); - } else if (vars[i] == "right") { - cycle.push_back(ORIENTATION_RIGHT); - } else if (vars[i] == "bottom") { - cycle.push_back(ORIENTATION_BOTTOM); - } else if (vars[i] == "left") { - cycle.push_back(ORIENTATION_LEFT); - } else if (vars[i] == "center") { - cycle.push_back(ORIENTATION_CENTER); - } - } -} - -eOrientation CHyprMasterLayout::getDynamicOrientation(PHLWORKSPACE pWorkspace) { - const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(pWorkspace); - std::string orientationString; - if (WORKSPACERULE.layoutopts.contains("orientation")) - orientationString = WORKSPACERULE.layoutopts.at("orientation"); - - eOrientation orientation = getMasterWorkspaceData(pWorkspace->m_id)->orientation; - // override if workspace rule is set - if (!orientationString.empty()) { - if (orientationString == "top") - orientation = ORIENTATION_TOP; - else if (orientationString == "right") - orientation = ORIENTATION_RIGHT; - else if (orientationString == "bottom") - orientation = ORIENTATION_BOTTOM; - else if (orientationString == "center") - orientation = ORIENTATION_CENTER; - else - orientation = ORIENTATION_LEFT; - } - - return orientation; -} - -void CHyprMasterLayout::replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) { - const auto PNODE = getNodeFromWindow(from); - - if (!PNODE) - return; - - PNODE->pWindow = to; - - applyNodeDataToWindow(PNODE); -} - -Vector2D CHyprMasterLayout::predictSizeForNewWindowTiled() { - static auto PNEWSTATUS = CConfigValue("master:new_status"); - - if (!Desktop::focusState()->monitor()) - return {}; - - const int NODES = getNodesOnWorkspace(Desktop::focusState()->monitor()->m_activeWorkspace->m_id); - - if (NODES <= 0) - return Desktop::focusState()->monitor()->m_size; - - const auto MASTER = getMasterNodeOnWorkspace(Desktop::focusState()->monitor()->m_activeWorkspace->m_id); - if (!MASTER) // wtf - return {}; - - if (*PNEWSTATUS == "master") { - return MASTER->size; - } else { - const auto SLAVES = NODES - getMastersOnWorkspace(Desktop::focusState()->monitor()->m_activeWorkspace->m_id); - - // TODO: make this better - return {Desktop::focusState()->monitor()->m_size.x - MASTER->size.x, Desktop::focusState()->monitor()->m_size.y / (SLAVES + 1)}; - } - - return {}; -} - -void CHyprMasterLayout::onEnable() { - for (auto const& w : g_pCompositor->m_windows) { - if (w->m_isFloating || !w->m_isMapped || w->isHidden()) - continue; - - onWindowCreatedTiling(w); - } -} - -void CHyprMasterLayout::onDisable() { - m_masterNodesData.clear(); -} diff --git a/src/layout/MasterLayout.hpp b/src/layout/MasterLayout.hpp deleted file mode 100644 index a5968916..00000000 --- a/src/layout/MasterLayout.hpp +++ /dev/null @@ -1,112 +0,0 @@ -#pragma once - -#include "IHyprLayout.hpp" -#include "../desktop/DesktopTypes.hpp" -#include "../helpers/varlist/VarList.hpp" -#include -#include -#include - -enum eFullscreenMode : int8_t; - -//orientation determines which side of the screen the master area resides -enum eOrientation : uint8_t { - ORIENTATION_LEFT = 0, - ORIENTATION_TOP, - ORIENTATION_RIGHT, - ORIENTATION_BOTTOM, - ORIENTATION_CENTER -}; - -struct SMasterNodeData { - bool isMaster = false; - float percMaster = 0.5f; - - PHLWINDOWREF pWindow; - - Vector2D position; - Vector2D size; - - float percSize = 1.f; // size multiplier for resizing children - - WORKSPACEID workspaceID = WORKSPACE_INVALID; - - bool ignoreFullscreenChecks = false; - - // - bool operator==(const SMasterNodeData& rhs) const { - return pWindow.lock() == rhs.pWindow.lock(); - } -}; - -struct SMasterWorkspaceData { - WORKSPACEID workspaceID = WORKSPACE_INVALID; - eOrientation orientation = ORIENTATION_LEFT; - // Previously focused non-master window when `focusmaster previous` command was issued - PHLWINDOWREF focusMasterPrev; - - // - bool operator==(const SMasterWorkspaceData& rhs) const { - return workspaceID == rhs.workspaceID; - } -}; - -class CHyprMasterLayout : public IHyprLayout { - public: - virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT); - virtual void onWindowRemovedTiling(PHLWINDOW); - virtual bool isWindowTiled(PHLWINDOW); - virtual void recalculateMonitor(const MONITORID&); - virtual void recalculateWindow(PHLWINDOW); - virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr); - virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE); - virtual std::any layoutMessage(SLayoutMessageHeader, std::string); - virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW); - virtual void switchWindows(PHLWINDOW, PHLWINDOW); - virtual void moveWindowTo(PHLWINDOW, const std::string& dir, bool silent); - virtual void alterSplitRatio(PHLWINDOW, float, bool); - virtual std::string getLayoutName(); - virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to); - virtual Vector2D predictSizeForNewWindowTiled(); - - virtual void onEnable(); - virtual void onDisable(); - - private: - std::list m_masterNodesData; - std::vector m_masterWorkspacesData; - - bool m_forceWarps = false; - - void buildOrientationCycleVectorFromVars(std::vector& cycle, CVarList& vars); - void buildOrientationCycleVectorFromEOperation(std::vector& cycle); - void runOrientationCycle(SLayoutMessageHeader& header, CVarList* vars, int next); - eOrientation getDynamicOrientation(PHLWORKSPACE); - int getNodesOnWorkspace(const WORKSPACEID&); - void applyNodeDataToWindow(SMasterNodeData*); - SMasterNodeData* getNodeFromWindow(PHLWINDOW); - SMasterNodeData* getMasterNodeOnWorkspace(const WORKSPACEID&); - SMasterWorkspaceData* getMasterWorkspaceData(const WORKSPACEID&); - void calculateWorkspace(PHLWORKSPACE); - PHLWINDOW getNextWindow(PHLWINDOW, bool, bool); - int getMastersOnWorkspace(const WORKSPACEID&); - - friend struct SMasterNodeData; - friend struct SMasterWorkspaceData; -}; - -template -struct std::formatter : std::formatter { - template - auto format(const SMasterNodeData* const& node, FormatContext& ctx) const { - auto out = ctx.out(); - if (!node) - return std::format_to(out, "[Node nullptr]"); - std::format_to(out, "[Node {:x}: workspace: {}, pos: {:j2}, size: {:j2}", rc(node), node->workspaceID, node->position, node->size); - if (node->isMaster) - std::format_to(out, ", master"); - if (!node->pWindow.expired()) - std::format_to(out, ", window: {:x}", node->pWindow.lock()); - return std::format_to(out, "]"); - } -}; diff --git a/src/layout/algorithm/Algorithm.cpp b/src/layout/algorithm/Algorithm.cpp new file mode 100644 index 00000000..cd8cfac4 --- /dev/null +++ b/src/layout/algorithm/Algorithm.cpp @@ -0,0 +1,264 @@ +#include "Algorithm.hpp" + +#include "FloatingAlgorithm.hpp" +#include "TiledAlgorithm.hpp" +#include "../target/WindowTarget.hpp" +#include "../space/Space.hpp" +#include "../../desktop/view/Window.hpp" +#include "../../desktop/history/WindowHistoryTracker.hpp" +#include "../../helpers/Monitor.hpp" +#include "../../render/Renderer.hpp" + +#include "../../debug/log/Logger.hpp" + +using namespace Layout; + +SP CAlgorithm::create(UP&& tiled, UP&& floating, SP space) { + auto algo = SP(new CAlgorithm(std::move(tiled), std::move(floating), space)); + algo->m_self = algo; + algo->m_tiled->m_parent = algo; + algo->m_floating->m_parent = algo; + return algo; +} + +CAlgorithm::CAlgorithm(UP&& tiled, UP&& floating, SP space) : + m_tiled(std::move(tiled)), m_floating(std::move(floating)), m_space(space) { + ; +} + +void CAlgorithm::addTarget(SP target) { + const bool SHOULD_FLOAT = target->floating(); + + if (SHOULD_FLOAT) { + m_floatingTargets.emplace_back(target); + m_floating->newTarget(target); + } else { + m_tiledTargets.emplace_back(target); + m_tiled->newTarget(target); + } +} + +void CAlgorithm::removeTarget(SP target) { + const bool IS_FLOATING = std::ranges::contains(m_floatingTargets, target); + + if (IS_FLOATING) { + m_floating->removeTarget(target); + std::erase(m_floatingTargets, target); + return; + } + + const bool IS_TILED = std::ranges::contains(m_tiledTargets, target); + + if (IS_TILED) { + m_tiled->removeTarget(target); + std::erase(m_tiledTargets, target); + return; + } + + Log::logger->log(Log::ERR, "BUG THIS: CAlgorithm::removeTarget, but not found"); +} + +void CAlgorithm::moveTarget(SP target, std::optional focalPoint, bool reposition) { + const bool SHOULD_FLOAT = target->floating(); + + if (SHOULD_FLOAT) { + m_floatingTargets.emplace_back(target); + if (reposition) + m_floating->newTarget(target); + else + m_floating->movedTarget(target, focalPoint); + } else { + m_tiledTargets.emplace_back(target); + if (reposition) + m_tiled->newTarget(target); + else + m_tiled->movedTarget(target, focalPoint); + } +} + +SP CAlgorithm::space() const { + return m_space.lock(); +} + +void CAlgorithm::setFloating(SP target, bool floating, bool reposition) { + removeTarget(target); + + g_pHyprRenderer->damageWindow(target->window()); + + target->setFloating(floating); + + moveTarget(target, std::nullopt, reposition); + + g_pHyprRenderer->damageWindow(target->window()); +} + +size_t CAlgorithm::tiledTargets() const { + return m_tiledTargets.size(); +} + +size_t CAlgorithm::floatingTargets() const { + return m_floatingTargets.size(); +} + +void CAlgorithm::recalculate() { + m_tiled->recalculate(); + m_floating->recalculate(); + + const auto PWORKSPACE = m_space->workspace(); + const auto PMONITOR = PWORKSPACE->m_monitor; + + if (PWORKSPACE->m_hasFullscreenWindow && PMONITOR) { + // massive hack from the fullscreen func + const auto PFULLWINDOW = PWORKSPACE->getFullscreenWindow(); + + if (PFULLWINDOW) { + if (PWORKSPACE->m_fullscreenMode == FSMODE_FULLSCREEN) { + *PFULLWINDOW->m_realPosition = PMONITOR->m_position; + *PFULLWINDOW->m_realSize = PMONITOR->m_size; + } else if (PWORKSPACE->m_fullscreenMode == FSMODE_MAXIMIZED) + PFULLWINDOW->layoutTarget()->setPositionGlobal(m_space->workArea()); + } + + return; + } +} + +void CAlgorithm::recenter(SP t) { + if (t->floating()) + m_floating->recenter(t); +} + +std::expected CAlgorithm::layoutMsg(const std::string_view& sv) { + if (const auto ret = m_floating->layoutMsg(sv); !ret) + return ret; + return m_tiled->layoutMsg(sv); +} + +std::optional CAlgorithm::predictSizeForNewTiledTarget() { + return m_tiled->predictSizeForNewTarget(); +} + +void CAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + if (target->floating()) + m_floating->resizeTarget(Δ, target, corner); + else + m_tiled->resizeTarget(Δ, target, corner); +} + +void CAlgorithm::moveTarget(const Vector2D& Δ, SP target) { + if (target->floating()) + m_floating->moveTarget(Δ, target); +} + +void CAlgorithm::swapTargets(SP a, SP b) { + auto swapFirst = [&a, &b](std::vector>& targets) -> bool { + auto ia = std::ranges::find(targets, a); + auto ib = std::ranges::find(targets, b); + + if (ia != std::ranges::end(targets) && ib != std::ranges::end(targets)) { + std::iter_swap(ia, ib); + return true; + } else if (ia != std::ranges::end(targets)) + *ia = b; + else if (ib != std::ranges::end(targets)) + *ib = a; + + return false; + }; + + if (!swapFirst(m_tiledTargets)) + swapFirst(m_floatingTargets); + + const WP algA = a->floating() ? WP(m_floating) : WP(m_tiled); + const WP algB = b->floating() ? WP(m_floating) : WP(m_tiled); + + algA->swapTargets(a, b); + if (algA != algB) + algB->swapTargets(b, a); +} + +void CAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + if (t->floating()) + m_floating->moveTargetInDirection(t, dir, silent); + else + m_tiled->moveTargetInDirection(t, dir, silent); +} + +void CAlgorithm::updateFloatingAlgo(UP&& algo) { + algo->m_parent = m_self; + + for (const auto& t : m_floatingTargets) { + const auto TARGET = t.lock(); + if (!TARGET) + continue; + + // Unhide windows when switching layouts to prevent them from being permanently lost + const auto WINDOW = TARGET->window(); + if (WINDOW) + WINDOW->setHidden(false); + + m_floating->removeTarget(TARGET); + algo->newTarget(TARGET); + } + + m_floating = std::move(algo); +} + +void CAlgorithm::updateTiledAlgo(UP&& algo) { + algo->m_parent = m_self; + + for (const auto& t : m_tiledTargets) { + const auto TARGET = t.lock(); + if (!TARGET) + continue; + + // Unhide windows when switching layouts to prevent them from being permanently lost + // This is a safeguard for layouts (including third-party plugins) that use setHidden + const auto WINDOW = TARGET->window(); + if (WINDOW) + WINDOW->setHidden(false); + + m_tiled->removeTarget(TARGET); + algo->newTarget(TARGET); + } + + m_tiled = std::move(algo); +} + +const UP& CAlgorithm::tiledAlgo() const { + return m_tiled; +} + +const UP& CAlgorithm::floatingAlgo() const { + return m_floating; +} + +SP CAlgorithm::getNextCandidate(SP old) { + if (old->floating()) { + // use window history to determine best target + for (const auto& w : Desktop::History::windowTracker()->fullHistory() | std::views::reverse) { + if (!w->m_workspace || w->m_workspace->m_space != m_space || !w->layoutTarget() || !w->layoutTarget()->space()) + continue; + + return w->layoutTarget(); + } + + // no history, fall back + } else { + // ask the layout + const auto CANDIDATE = m_tiled->getNextCandidate(old); + if (CANDIDATE) + return CANDIDATE; + + // no candidate, fall back + } + + // fallback: try to focus anything + if (!m_tiledTargets.empty()) + return m_tiledTargets.back().lock(); + if (!m_floatingTargets.empty()) + return m_floatingTargets.back().lock(); + + // god damn it, maybe empty? + return nullptr; +} diff --git a/src/layout/algorithm/Algorithm.hpp b/src/layout/algorithm/Algorithm.hpp new file mode 100644 index 00000000..3ee26a3c --- /dev/null +++ b/src/layout/algorithm/Algorithm.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/math/Direction.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include "../LayoutManager.hpp" + +#include +#include + +namespace Layout { + class ITarget; + class IFloatingAlgorithm; + class ITiledAlgorithm; + class CSpace; + + class CAlgorithm { + public: + static SP create(UP&& tiled, UP&& floating, SP space); + ~CAlgorithm() = default; + + void addTarget(SP target); + void moveTarget(SP target, std::optional focalPoint = std::nullopt, bool reposition = false); + void removeTarget(SP target); + + void swapTargets(SP a, SP b); + void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + SP getNextCandidate(SP old); + + void setFloating(SP target, bool floating, bool reposition = false); + + std::expected layoutMsg(const std::string_view& sv); + std::optional predictSizeForNewTiledTarget(); + + void recalculate(); + void recenter(SP t); + + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); + + void updateFloatingAlgo(UP&& algo); + void updateTiledAlgo(UP&& algo); + + const UP& tiledAlgo() const; + const UP& floatingAlgo() const; + + SP space() const; + + size_t tiledTargets() const; + size_t floatingTargets() const; + + private: + CAlgorithm(UP&& tiled, UP&& floating, SP space); + + UP m_tiled; + UP m_floating; + WP m_space; + WP m_self; + + std::vector> m_tiledTargets, m_floatingTargets; + }; +} \ No newline at end of file diff --git a/src/layout/algorithm/FloatingAlgorithm.cpp b/src/layout/algorithm/FloatingAlgorithm.cpp new file mode 100644 index 00000000..058887bf --- /dev/null +++ b/src/layout/algorithm/FloatingAlgorithm.cpp @@ -0,0 +1,18 @@ +#include "FloatingAlgorithm.hpp" +#include "Algorithm.hpp" +#include "../space/Space.hpp" + +using namespace Layout; + +void IFloatingAlgorithm::recalculate() { + ; +} + +void IFloatingAlgorithm::recenter(SP t) { + const auto LAST = t->lastFloatingSize(); + + if (LAST.x <= 5 || LAST.y <= 5) + return; + + t->setPositionGlobal({m_parent->space()->workArea().middle() - LAST / 2.F, LAST}); +} diff --git a/src/layout/algorithm/FloatingAlgorithm.hpp b/src/layout/algorithm/FloatingAlgorithm.hpp new file mode 100644 index 00000000..2c9ff14b --- /dev/null +++ b/src/layout/algorithm/FloatingAlgorithm.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include "ModeAlgorithm.hpp" + +namespace Layout { + + class ITarget; + class CAlgorithm; + + class IFloatingAlgorithm : public IModeAlgorithm { + public: + virtual ~IFloatingAlgorithm() = default; + + // a target is being moved by a delta + virtual void moveTarget(const Vector2D& Δ, SP target) = 0; + + virtual void recenter(SP t); + + virtual void recalculate(); + + protected: + IFloatingAlgorithm() = default; + + WP m_parent; + + friend class Layout::CAlgorithm; + }; +} \ No newline at end of file diff --git a/src/layout/algorithm/ModeAlgorithm.cpp b/src/layout/algorithm/ModeAlgorithm.cpp new file mode 100644 index 00000000..261c54da --- /dev/null +++ b/src/layout/algorithm/ModeAlgorithm.cpp @@ -0,0 +1,11 @@ +#include "ModeAlgorithm.hpp" + +using namespace Layout; + +std::expected IModeAlgorithm::layoutMsg(const std::string_view& sv) { + return {}; +} + +std::optional IModeAlgorithm::predictSizeForNewTarget() { + return std::nullopt; +} diff --git a/src/layout/algorithm/ModeAlgorithm.hpp b/src/layout/algorithm/ModeAlgorithm.hpp new file mode 100644 index 00000000..90d7ce58 --- /dev/null +++ b/src/layout/algorithm/ModeAlgorithm.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/math/Direction.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include "../LayoutManager.hpp" + +#include + +namespace Layout { + + class ITarget; + class CAlgorithm; + + class IModeAlgorithm { + public: + virtual ~IModeAlgorithm() = default; + + // a completely new target + virtual void newTarget(SP target) = 0; + + // a target moved into the algorithm (from another) + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt) = 0; + + // a target removed + virtual void removeTarget(SP target) = 0; + + // a target is being resized by a delta. Corner none likely means not interactive + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE) = 0; + + // recalculate layout + virtual void recalculate() = 0; + + // swap targets + virtual void swapTargets(SP a, SP b) = 0; + + // move a target in a given direction + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent) = 0; + + // optional: handle layout messages + virtual std::expected layoutMsg(const std::string_view& sv); + + // optional: predict new window's size + virtual std::optional predictSizeForNewTarget(); + + protected: + IModeAlgorithm() = default; + + WP m_parent; + + friend class Layout::CAlgorithm; + }; +} \ No newline at end of file diff --git a/src/layout/algorithm/TiledAlgorithm.hpp b/src/layout/algorithm/TiledAlgorithm.hpp new file mode 100644 index 00000000..99d1bd99 --- /dev/null +++ b/src/layout/algorithm/TiledAlgorithm.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include "ModeAlgorithm.hpp" + +namespace Layout { + + class ITarget; + class CAlgorithm; + + class ITiledAlgorithm : public IModeAlgorithm { + public: + virtual ~ITiledAlgorithm() = default; + + virtual SP getNextCandidate(SP old) = 0; + + protected: + ITiledAlgorithm() = default; + + WP m_parent; + + friend class Layout::CAlgorithm; + }; +} \ No newline at end of file diff --git a/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp new file mode 100644 index 00000000..7fb8ec7e --- /dev/null +++ b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp @@ -0,0 +1,223 @@ +#include "DefaultFloatingAlgorithm.hpp" + +#include "../../Algorithm.hpp" + +#include "../../../target/WindowTarget.hpp" +#include "../../../space/Space.hpp" + +#include "../../../../Compositor.hpp" +#include "../../../../helpers/Monitor.hpp" + +using namespace Layout; +using namespace Layout::Floating; + +constexpr const Vector2D DEFAULT_SIZE = {640, 400}; + +// +void CDefaultFloatingAlgorithm::newTarget(SP target) { + const auto WORK_AREA = m_parent->space()->workArea(true); + const auto DESIRED_GEOM = target->desiredGeometry(); + const auto MONITOR_POS = m_parent->space()->workspace()->m_monitor->logicalBox().pos(); + + CBox windowGeometry; + + if (!DESIRED_GEOM) { + switch (DESIRED_GEOM.error()) { + case GEOMETRY_INVALID_DESIRED: { + // if the desired is invalid, we hide the window. + if (target->type() == TARGET_TYPE_WINDOW) + dynamicPointerCast(target)->window()->setHidden(true); + return; + } + case GEOMETRY_NO_DESIRED: { + // add a default geom + windowGeometry = CBox{WORK_AREA.middle() - DEFAULT_SIZE / 2.F, DEFAULT_SIZE}; + break; + } + } + } else { + if (DESIRED_GEOM->pos) + windowGeometry = CBox{DESIRED_GEOM->pos.value(), DESIRED_GEOM->size}; + else + windowGeometry = CBox{WORK_AREA.middle() - DESIRED_GEOM->size / 2.F, DESIRED_GEOM->size}; + } + + bool posOverridden = false; + + if (target->window() && target->window()->m_firstMap) { + const auto WINDOW = target->window(); + + // set this here so that expressions can use it. This could be wrong of course. + WINDOW->m_realSize->setValueAndWarp(DESIRED_GEOM ? DESIRED_GEOM->size : DEFAULT_SIZE); + + if (!WINDOW->m_ruleApplicator->static_.size.empty()) { + const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.size); + if (!COMPUTED) + Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.size); + else { + windowGeometry.w = COMPUTED->x; + windowGeometry.h = COMPUTED->y; + + // update for pos to work with size. + WINDOW->m_realPosition->setValueAndWarp(*COMPUTED); + } + } + + if (!WINDOW->m_ruleApplicator->static_.position.empty()) { + const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.position); + if (!COMPUTED) + Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.position); + else { + windowGeometry.x = COMPUTED->x + MONITOR_POS.x; + windowGeometry.y = COMPUTED->y + MONITOR_POS.y; + posOverridden = true; + } + } + + if (WINDOW->m_ruleApplicator->static_.center.value_or(false)) { + const auto POS = WORK_AREA.middle() - windowGeometry.size() / 2.f; + windowGeometry.x = POS.x; + windowGeometry.y = POS.y; + posOverridden = true; + } + } else if (target->lastFloatingSize().x > 5 && target->lastFloatingSize().y > 5) { + windowGeometry.w = target->lastFloatingSize().x; + windowGeometry.h = target->lastFloatingSize().y; + } + + if (!posOverridden && (!DESIRED_GEOM || !DESIRED_GEOM->pos)) + windowGeometry = CBox{WORK_AREA.middle() - windowGeometry.size() / 2.F, windowGeometry.size()}; + + if (posOverridden || WORK_AREA.containsPoint(windowGeometry.middle())) + target->setPositionGlobal(windowGeometry); + else { + const auto POS = WORK_AREA.middle() - windowGeometry.size() / 2.f; + windowGeometry.x = POS.x; + windowGeometry.y = POS.y; + + target->setPositionGlobal(windowGeometry); + } + + // TODO: not very OOP, is it? + if (const auto WTARGET = dynamicPointerCast(target); WTARGET) { + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + + const auto PWINDOW = WTARGET->window(); + const auto PMONITOR = WTARGET->space()->workspace()->m_monitor.lock(); + + if (*PXWLFORCESCALEZERO && PWINDOW->m_isX11) + *PWINDOW->m_realSize = PWINDOW->m_realSize->goal() / PMONITOR->m_scale; + + if (PWINDOW->m_X11DoesntWantBorders || (PWINDOW->m_isX11 && PWINDOW->isX11OverrideRedirect())) { + PWINDOW->m_realPosition->warp(); + PWINDOW->m_realSize->warp(); + } + + if (!PWINDOW->isX11OverrideRedirect()) + g_pCompositor->changeWindowZOrder(PWINDOW, true); + else { + PWINDOW->m_pendingReportedSize = PWINDOW->m_realSize->goal(); + PWINDOW->m_reportedSize = PWINDOW->m_pendingReportedSize; + } + } +} + +void CDefaultFloatingAlgorithm::movedTarget(SP target, std::optional focalPoint) { + auto LAST_SIZE = target->lastFloatingSize(); + const auto CURRENT_SIZE = target->position().size(); + + // ignore positioning a dragged target + if (g_layoutManager->dragController()->target() == target) + return; + + if (LAST_SIZE.x < 5 || LAST_SIZE.y < 5) { + const auto DESIRED = target->desiredGeometry(); + LAST_SIZE = DESIRED ? DESIRED->size : DEFAULT_SIZE; + } + + if (target->wasTiling()) { + // Avoid floating toggles that don't change size, they aren't easily visible to the user + if (std::abs(LAST_SIZE.x - CURRENT_SIZE.x) < 5 && std::abs(LAST_SIZE.y - CURRENT_SIZE.y) < 5) + LAST_SIZE += Vector2D{10, 10}; + + // calculate new position + const auto OLD_CENTER = target->position().middle(); + + // put around the current center, fit in workArea + target->setPositionGlobal(fitBoxInWorkArea(CBox{OLD_CENTER - LAST_SIZE / 2.F, LAST_SIZE}, target)); + + } else { + // calculate new position + const auto THIS_MON_POS = m_parent->space()->workspace()->m_monitor->m_position; + const auto OLD_POS = target->position().pos(); + const auto MON_FROM_OLD = g_pCompositor->getMonitorFromVector(OLD_POS); + const auto NEW_POS = MON_FROM_OLD ? OLD_POS - MON_FROM_OLD->m_position + THIS_MON_POS : OLD_POS; + + // put around the current center, fit in workArea + target->setPositionGlobal(fitBoxInWorkArea(CBox{NEW_POS, LAST_SIZE}, target)); + } +} + +CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP t) { + const auto WORK_AREA = m_parent->space()->workArea(true); + const auto EXTENTS = t->window() ? t->window()->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS) : SBoxExtents{}; + CBox targetBox = box.copy().addExtents(EXTENTS); + + targetBox.x = std::max(targetBox.x, WORK_AREA.x); + targetBox.y = std::max(targetBox.y, WORK_AREA.y); + + if (targetBox.x + targetBox.w > WORK_AREA.x + WORK_AREA.w) + targetBox.x = WORK_AREA.x + WORK_AREA.w - targetBox.w; + + if (targetBox.y + targetBox.h > WORK_AREA.y + WORK_AREA.h) + targetBox.y = WORK_AREA.y + WORK_AREA.h - targetBox.h; + + return targetBox.addExtents(SBoxExtents{.topLeft = -EXTENTS.topLeft, .bottomRight = -EXTENTS.bottomRight}); +} + +void CDefaultFloatingAlgorithm::removeTarget(SP target) { + target->rememberFloatingSize(target->position().size()); +} + +void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + auto pos = target->position(); + pos.w += Δ.x; + pos.h += Δ.y; + pos.translate(-Δ / 2.F); + target->setPositionGlobal(pos); + + if (g_layoutManager->dragController()->target() == target) + target->warpPositionSize(); +} + +void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP target) { + auto pos = target->position(); + pos.translate(Δ); + target->setPositionGlobal(pos); + + if (g_layoutManager->dragController()->target() == target) + target->warpPositionSize(); +} + +void CDefaultFloatingAlgorithm::swapTargets(SP a, SP b) { + auto posABackup = a->position(); + a->setPositionGlobal(b->position()); + b->setPositionGlobal(posABackup); +} + +void CDefaultFloatingAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + auto pos = t->position(); + auto work = m_parent->space()->workArea(true); + + const auto EXTENTS = t->window() ? t->window()->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS) : SBoxExtents{}; + + switch (dir) { + case Math::DIRECTION_LEFT: pos.x = work.x + EXTENTS.topLeft.x; break; + case Math::DIRECTION_RIGHT: pos.x = work.x + work.w - pos.w - EXTENTS.bottomRight.x; break; + case Math::DIRECTION_UP: pos.y = work.y + EXTENTS.topLeft.y; break; + case Math::DIRECTION_DOWN: pos.y = work.y + work.h - pos.h - EXTENTS.bottomRight.y; break; + default: Log::logger->log(Log::ERR, "Invalid direction in CDefaultFloatingAlgorithm::moveTargetInDirection"); break; + } + + t->setPositionGlobal(pos); +} diff --git a/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.hpp b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.hpp new file mode 100644 index 00000000..ef94e371 --- /dev/null +++ b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.hpp @@ -0,0 +1,26 @@ +#include "../../FloatingAlgorithm.hpp" + +namespace Layout { + class CAlgorithm; +} + +namespace Layout::Floating { + class CDefaultFloatingAlgorithm : public IFloatingAlgorithm { + public: + CDefaultFloatingAlgorithm() = default; + virtual ~CDefaultFloatingAlgorithm() = default; + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void moveTarget(const Vector2D& Δ, SP target); + + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + private: + CBox fitBoxInWorkArea(const CBox& box, SP t); + }; +}; \ No newline at end of file diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp new file mode 100644 index 00000000..5b90bb46 --- /dev/null +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp @@ -0,0 +1,772 @@ +#include "DwindleAlgorithm.hpp" + +#include "../../Algorithm.hpp" +#include "../../../space/Space.hpp" +#include "../../../target/WindowTarget.hpp" +#include "../../../LayoutManager.hpp" + +#include "../../../../config/ConfigValue.hpp" +#include "../../../../desktop/state/FocusState.hpp" +#include "../../../../helpers/Monitor.hpp" +#include "../../../../Compositor.hpp" + +#include + +using namespace Layout; +using namespace Layout::Tiled; + +struct Layout::Tiled::SDwindleNodeData { + WP pParent; + bool isNode = false; + WP pTarget; + std::array, 2> children = {}; + WP self; + bool splitTop = false; // for preserve_split + CBox box = {0}; + float splitRatio = 1.f; + bool valid = true; + bool ignoreFullscreenChecks = false; + + // For list lookup + bool operator==(const SDwindleNodeData& rhs) const { + return pTarget.lock() == rhs.pTarget.lock() && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] && children[1] == rhs.children[1]; + } + + void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false) { + 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"); + + if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0) + splitTop = box.h * *PFLMULT > box.w; + + if (verticalOverride) + splitTop = true; + else if (horizontalOverride) + splitTop = false; + + const auto SPLITSIDE = !splitTop; + + 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(); + } 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(); + } + + children[0]->recalcSizePosRecursive(force); + children[1]->recalcSizePosRecursive(force); + } else + pTarget->setPositionGlobal(box); + } +}; + +void CDwindleAlgorithm::newTarget(SP target) { + addTarget(target); +} + +void CDwindleAlgorithm::addTarget(SP target, bool newTarget) { + const auto WORK_AREA = m_parent->space()->workArea(); + + const auto PNODE = m_dwindleNodesData.emplace_back(makeShared()); + PNODE->self = PNODE; + + const auto PMONITOR = m_parent->space()->workspace()->m_monitor; + const auto PWORKSPACE = m_parent->space()->workspace(); + + static auto PUSEACTIVE = CConfigValue("dwindle:use_active_for_splits"); + static auto PDEFAULTSPLIT = CConfigValue("dwindle:default_split_ratio"); + + // Populate the node with our window's data + PNODE->pTarget = target; + PNODE->isNode = false; + + SP OPENINGON; + + const auto MOUSECOORDS = m_overrideFocalPoint.value_or(g_pInputManager->getMouseCoordsInternal()); + const auto ACTIVE_MON = Desktop::focusState()->monitor(); + + if ((PWORKSPACE == ACTIVE_MON->m_activeWorkspace || (PWORKSPACE->m_isSpecialWorkspace && PMONITOR->m_activeSpecialWorkspace)) && !*PUSEACTIVE) { + OPENINGON = getNodeFromWindow( + g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::SKIP_FULLSCREEN_PRIORITY)); + + if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON)) + OPENINGON = getClosestNode(MOUSECOORDS); + + } else if (*PUSEACTIVE) { + if (Desktop::focusState()->window() && !Desktop::focusState()->window()->m_isFloating && Desktop::focusState()->window() != target->window() && + Desktop::focusState()->window()->m_workspace == PWORKSPACE && Desktop::focusState()->window()->m_isMapped) { + OPENINGON = getNodeFromWindow(Desktop::focusState()->window()); + } else { + OPENINGON = getNodeFromWindow(g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS)); + } + + if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON)) + OPENINGON = getClosestNode(MOUSECOORDS); + + } else + OPENINGON = getFirstNode(); + + // first, check if OPENINGON isn't too big. + const auto PREDSIZEMAX = OPENINGON ? Vector2D(OPENINGON->box.w, OPENINGON->box.h) : PMONITOR->m_size; + if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PREDSIZEMAX.x || MAXSIZE.y < PREDSIZEMAX.y) { + // we can't continue. make it floating. + std::erase(m_dwindleNodesData, PNODE); + m_parent->setFloating(target, true, true); + return; + } + + // last fail-safe to avoid duplicate fullscreens + if ((!OPENINGON || OPENINGON->pTarget.lock() == target) && getNodes() > 1) { + for (auto& node : m_dwindleNodesData) { + if (node->pTarget.lock() && node->pTarget.lock() != target) { + OPENINGON = node; + break; + } + } + } + + // if it's the first, it's easy. Make it fullscreen. + if (!OPENINGON || OPENINGON->pTarget.lock() == target) { + PNODE->box = WORK_AREA; + PNODE->pTarget->setPositionGlobal(PNODE->box); + return; + } + + // get the node under our cursor + + const auto NEWPARENT = m_dwindleNodesData.emplace_back(makeShared()); + + // make the parent have the OPENINGON's stats + NEWPARENT->box = OPENINGON->box; + NEWPARENT->pParent = OPENINGON->pParent; + NEWPARENT->isNode = true; // it is a node + NEWPARENT->splitRatio = std::clamp(*PDEFAULTSPLIT, 0.1F, 1.9F); + + static auto PWIDTHMULTIPLIER = CConfigValue("dwindle:split_width_multiplier"); + + // if cursor over first child, make it first, etc + const auto SIDEBYSIDE = NEWPARENT->box.w > NEWPARENT->box.h * *PWIDTHMULTIPLIER; + NEWPARENT->splitTop = !SIDEBYSIDE; + + static auto PFORCESPLIT = CConfigValue("dwindle:force_split"); + static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue("dwindle:permanent_direction_override"); + static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); + static auto PSPLITBIAS = CConfigValue("dwindle:split_bias"); + + bool horizontalOverride = false; + bool verticalOverride = false; + + // let user select position -> top, right, bottom, left + if (m_overrideDirection != Math::DIRECTION_DEFAULT) { + + // this is horizontal + if (m_overrideDirection % 2 == 0) + verticalOverride = true; + else + horizontalOverride = true; + + // 0 -> top and left | 1,2 -> right and bottom + if (m_overrideDirection % 3 == 0) { + NEWPARENT->children[1] = OPENINGON; + NEWPARENT->children[0] = PNODE; + } else { + NEWPARENT->children[0] = OPENINGON; + NEWPARENT->children[1] = PNODE; + } + + // whether or not the override persists after opening one window + if (*PERMANENTDIRECTIONOVERRIDE == 0) + m_overrideDirection = Math::DIRECTION_DEFAULT; + } else if (*PSMARTSPLIT == 1) { + const auto PARENT_CENTER = NEWPARENT->box.pos() + NEWPARENT->box.size() / 2; + const auto PARENT_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w; + const auto DELTA = MOUSECOORDS - PARENT_CENTER; + const auto DELTA_SLOPE = DELTA.y / DELTA.x; + + if (abs(DELTA_SLOPE) < PARENT_PROPORTIONS) { + if (DELTA.x > 0) { + // right + NEWPARENT->splitTop = false; + NEWPARENT->children[0] = OPENINGON; + NEWPARENT->children[1] = PNODE; + } else { + // left + NEWPARENT->splitTop = false; + NEWPARENT->children[0] = PNODE; + NEWPARENT->children[1] = OPENINGON; + } + } else { + if (DELTA.y > 0) { + // bottom + NEWPARENT->splitTop = true; + NEWPARENT->children[0] = OPENINGON; + NEWPARENT->children[1] = PNODE; + } else { + // top + NEWPARENT->splitTop = true; + NEWPARENT->children[0] = PNODE; + NEWPARENT->children[1] = OPENINGON; + } + } + } else if (*PFORCESPLIT == 0 || !newTarget) { + if ((SIDEBYSIDE && + VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y + NEWPARENT->box.h)) || + (!SIDEBYSIDE && + VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w, NEWPARENT->box.y + NEWPARENT->box.h / 2.f))) { + // we are hovering over the first node, make PNODE first. + NEWPARENT->children[1] = OPENINGON; + NEWPARENT->children[0] = PNODE; + } else { + // we are hovering over the second node, make PNODE second. + NEWPARENT->children[0] = OPENINGON; + NEWPARENT->children[1] = PNODE; + } + } else { + if (*PFORCESPLIT == 1) { + NEWPARENT->children[1] = OPENINGON; + NEWPARENT->children[0] = PNODE; + } else { + NEWPARENT->children[0] = OPENINGON; + NEWPARENT->children[1] = PNODE; + } + } + + // split in favor of a specific window + if (*PSPLITBIAS && NEWPARENT->children[0] == PNODE) + NEWPARENT->splitRatio = 2.f - NEWPARENT->splitRatio; + + // and update the previous parent if it exists + if (OPENINGON->pParent) { + if (OPENINGON->pParent->children[0] == OPENINGON) { + OPENINGON->pParent->children[0] = NEWPARENT; + } else { + OPENINGON->pParent->children[1] = NEWPARENT; + } + } + + // Update the children + if (!verticalOverride && (NEWPARENT->box.w * *PWIDTHMULTIPLIER > NEWPARENT->box.h || horizontalOverride)) { + // split left/right -> forced + OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)}; + PNODE->box = {Vector2D(NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)}; + } else { + // split top/bottom + OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)}; + PNODE->box = {Vector2D(NEWPARENT->box.x, NEWPARENT->box.y + NEWPARENT->box.h / 2.f), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)}; + } + + OPENINGON->pParent = NEWPARENT; + PNODE->pParent = NEWPARENT; + + NEWPARENT->recalcSizePosRecursive(false, horizontalOverride, verticalOverride); + + calculateWorkspace(); +} + +void CDwindleAlgorithm::movedTarget(SP target, std::optional focalPoint) { + m_overrideFocalPoint = focalPoint; + addTarget(target, false); + m_overrideFocalPoint.reset(); +} + +void CDwindleAlgorithm::removeTarget(SP target) { + const auto PNODE = getNodeFromTarget(target); + + if (!PNODE) { + Log::logger->log(Log::ERR, "onWindowRemovedTiling node null?"); + return; + } + + if (target->fullscreenMode() != FSMODE_NONE) + g_pCompositor->setWindowFullscreenInternal(target->window(), FSMODE_NONE); + + const auto PPARENT = PNODE->pParent; + + if (!PPARENT) { + Log::logger->log(Log::DEBUG, "Removing last node (dwindle)"); + std::erase(m_dwindleNodesData, PNODE); + return; + } + + const auto PSIBLING = PPARENT->children[0] == PNODE ? PPARENT->children[1] : PPARENT->children[0]; + + PSIBLING->pParent = PPARENT->pParent; + + if (PPARENT->pParent != nullptr) { + if (PPARENT->pParent->children[0] == PPARENT) + PPARENT->pParent->children[0] = PSIBLING; + else + PPARENT->pParent->children[1] = PSIBLING; + } + + PPARENT->valid = false; + PNODE->valid = false; + + std::erase(m_dwindleNodesData, PPARENT); + std::erase(m_dwindleNodesData, PNODE); + + recalculate(); +} + +void CDwindleAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + if (!validMapped(target->window())) + return; + + const auto PNODE = getNodeFromTarget(target); + + if (!PNODE) + return; + + static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); + static auto PSMARTRESIZING = CConfigValue("dwindle:smart_resizing"); + + // get some data about our window + const auto PMONITOR = m_parent->space()->workspace()->m_monitor; + const auto MONITOR_WORKAREA = PMONITOR->logicalBoxMinusReserved(); + const auto BOX = target->position(); + const bool DISPLAYLEFT = STICKS(BOX.x, MONITOR_WORKAREA.x); + const bool DISPLAYRIGHT = STICKS(BOX.x + BOX.w, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); + const bool DISPLAYTOP = STICKS(BOX.y, MONITOR_WORKAREA.y); + const bool DISPLAYBOTTOM = STICKS(BOX.y + BOX.h, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h); + + // construct allowed movement + Vector2D allowedMovement = Δ; + if (DISPLAYLEFT && DISPLAYRIGHT) + allowedMovement.x = 0; + + if (DISPLAYBOTTOM && DISPLAYTOP) + allowedMovement.y = 0; + + if (*PSMARTRESIZING == 1) { + // Identify inner and outer nodes for both directions + SP PVOUTER = nullptr; + SP PVINNER = nullptr; + SP PHOUTER = nullptr; + SP 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 NONE = corner == CORNER_NONE; + + for (auto PCURRENT = PNODE; PCURRENT && PCURRENT->pParent; PCURRENT = PCURRENT->pParent.lock()) { + const auto PPARENT = PCURRENT->pParent; + + if (!PVOUTER && PPARENT->splitTop && (NONE || (TOP && PPARENT->children[1] == PCURRENT) || (BOTTOM && PPARENT->children[0] == PCURRENT))) + PVOUTER = PCURRENT; + else if (!PVOUTER && !PVINNER && PPARENT->splitTop) + PVINNER = PCURRENT; + else if (!PHOUTER && !PPARENT->splitTop && (NONE || (LEFT && PPARENT->children[1] == PCURRENT) || (RIGHT && PPARENT->children[0] == PCURRENT))) + PHOUTER = PCURRENT; + else if (!PHOUTER && !PHINNER && !PPARENT->splitTop) + PHINNER = PCURRENT; + + if (PVOUTER && PHOUTER) + break; + } + + if (PHOUTER) { + PHOUTER->pParent->splitRatio = std::clamp(PHOUTER->pParent->splitRatio + allowedMovement.x * 2.f / PHOUTER->pParent->box.w, 0.1, 1.9); + + if (PHINNER) { + const auto ORIGINAL = PHINNER->box.w; + PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + if (PHINNER->pParent->children[0] == PHINNER) + PHINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9); + else + PHINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9); + PHINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + } else + PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + } + + if (PVOUTER) { + PVOUTER->pParent->splitRatio = std::clamp(PVOUTER->pParent->splitRatio + allowedMovement.y * 2.f / PVOUTER->pParent->box.h, 0.1, 1.9); + + if (PVINNER) { + const auto ORIGINAL = PVINNER->box.h; + PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + if (PVINNER->pParent->children[0] == PVINNER) + PVINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9); + else + PVINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9); + PVINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + } else + PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0); + } + } else { + // get the correct containers to apply splitratio to + const auto PPARENT = PNODE->pParent; + + if (!PPARENT) + return; // the only window on a workspace, ignore + + const bool PARENTSIDEBYSIDE = !PPARENT->splitTop; + + // Get the parent's parent + auto PPARENT2 = PPARENT->pParent; + + Hyprutils::Utils::CScopeGuard x([target, this] { + // snap all windows, don't animate resizes if they are manual + if (target == g_layoutManager->dragController()->target()) { + for (const auto& w : m_dwindleNodesData) { + if (w->isNode) + continue; + + w->pTarget->warpPositionSize(); + } + } + }); + + // No parent means we have only 2 windows, and thus one axis of freedom + if (!PPARENT2) { + if (PARENTSIDEBYSIDE) { + allowedMovement.x *= 2.f / PPARENT->box.w; + PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9); + PPARENT->recalcSizePosRecursive(*PANIMATE == 0); + } else { + allowedMovement.y *= 2.f / PPARENT->box.h; + PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9); + PPARENT->recalcSizePosRecursive(*PANIMATE == 0); + } + + return; + } + + // Get first parent with other split + while (PPARENT2 && PPARENT2->splitTop == !PARENTSIDEBYSIDE) + PPARENT2 = PPARENT2->pParent; + + // no parent, one axis of freedom + if (!PPARENT2) { + if (PARENTSIDEBYSIDE) { + allowedMovement.x *= 2.f / PPARENT->box.w; + PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9); + PPARENT->recalcSizePosRecursive(*PANIMATE == 0); + } else { + allowedMovement.y *= 2.f / PPARENT->box.h; + PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9); + PPARENT->recalcSizePosRecursive(*PANIMATE == 0); + } + + return; + } + + // 2 axes of freedom + const auto SIDECONTAINER = PARENTSIDEBYSIDE ? PPARENT : PPARENT2; + const auto TOPCONTAINER = PARENTSIDEBYSIDE ? PPARENT2 : PPARENT; + + allowedMovement.x *= 2.f / SIDECONTAINER->box.w; + allowedMovement.y *= 2.f / TOPCONTAINER->box.h; + + SIDECONTAINER->splitRatio = std::clamp(SIDECONTAINER->splitRatio + allowedMovement.x, 0.1, 1.9); + TOPCONTAINER->splitRatio = std::clamp(TOPCONTAINER->splitRatio + allowedMovement.y, 0.1, 1.9); + SIDECONTAINER->recalcSizePosRecursive(*PANIMATE == 0); + TOPCONTAINER->recalcSizePosRecursive(*PANIMATE == 0); + } + + // snap all windows, don't animate resizes if they are manual + if (target == g_layoutManager->dragController()->target()) { + for (const auto& w : m_dwindleNodesData) { + if (w->isNode) + continue; + + w->pTarget->warpPositionSize(); + } + } +} + +SP CDwindleAlgorithm::getNextCandidate(SP old) { + const auto MIDDLE = old->position().middle(); + + if (const auto NODE = getClosestNode(MIDDLE); NODE) + return NODE->pTarget.lock(); + + if (const auto NODE = getFirstNode(); NODE) + return NODE->pTarget.lock(); + + return nullptr; +} + +void CDwindleAlgorithm::swapTargets(SP a, SP b) { + auto nodeA = getNodeFromTarget(a); + auto nodeB = getNodeFromTarget(b); + + if (nodeA) + nodeA->pTarget = b; + if (nodeB) + nodeB->pTarget = a; +} + +void CDwindleAlgorithm::recalculate() { + calculateWorkspace(); +} + +std::optional CDwindleAlgorithm::predictSizeForNewTarget() { + // get window candidate + PHLWINDOW candidate = Desktop::focusState()->window(); + + if (!candidate || candidate->m_workspace != m_parent->space()->workspace()) + candidate = m_parent->space()->workspace()->getFirstWindow(); + + // create a fake node + SDwindleNodeData node; + + if (!candidate) + return Desktop::focusState()->monitor()->m_size; + else { + const auto PNODE = getNodeFromWindow(candidate); + + if (!PNODE) + return {}; + + node = *PNODE; + node.pTarget.reset(); + + CBox box = PNODE->box; + + static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); + + bool splitTop = box.h * *PFLMULT > box.w; + + const auto SPLITSIDE = !splitTop; + + if (SPLITSIDE) + node.box = {{}, {box.w / 2.0, box.h}}; + else + node.box = {{}, {box.w, box.h / 2.0}}; + + // TODO: make this better and more accurate + + return node.box.size(); + } + + return {}; +} + +void CDwindleAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + const auto PNODE = getNodeFromTarget(t); + const Vector2D originalPos = t->position().middle(); + + if (!PNODE || !t->window()) + return; + + Vector2D focalPoint; + + const auto WINDOWIDEALBB = + t->fullscreenMode() != FSMODE_NONE ? m_parent->space()->workspace()->m_monitor->logicalBox() : t->window()->getWindowIdealBoundingBoxIgnoreReserved(); + + switch (dir) { + case Math::DIRECTION_UP: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, -1.0}; break; + case Math::DIRECTION_DOWN: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, WINDOWIDEALBB.size().y + 1.0}; break; + case Math::DIRECTION_LEFT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{-1.0, WINDOWIDEALBB.size().y / 2.0}; break; + case Math::DIRECTION_RIGHT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x + 1.0, WINDOWIDEALBB.size().y / 2.0}; break; + default: return; + } + + t->window()->setAnimationsToMove(); + + removeTarget(t); + + const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(focalPoint); + + if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor) { + // move with a focal point + + if (PMONITORFOCAL->m_activeWorkspace) + t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space); + + return; + } + + movedTarget(t, focalPoint); + + // restore focus to the previous position + if (silent) { + const auto PNODETOFOCUS = getClosestNode(originalPos); + if (PNODETOFOCUS && PNODETOFOCUS->pTarget) + Desktop::focusState()->fullWindowFocus(PNODETOFOCUS->pTarget->window(), Desktop::FOCUS_REASON_KEYBIND); + } +} + +// --------- internal --------- // + +void CDwindleAlgorithm::calculateWorkspace() { + const auto PWORKSPACE = m_parent->space()->workspace(); + const auto PMONITOR = PWORKSPACE->m_monitor; + + if (!PMONITOR || PWORKSPACE->m_hasFullscreenWindow) + return; + + const auto TOPNODE = getMasterNode(); + + if (TOPNODE) { + TOPNODE->box = m_parent->space()->workArea(); + TOPNODE->recalcSizePosRecursive(); + } +} + +SP CDwindleAlgorithm::getNodeFromTarget(SP t) { + for (const auto& n : m_dwindleNodesData) { + if (n->pTarget == t) + return n; + } + + return nullptr; +} + +SP CDwindleAlgorithm::getNodeFromWindow(PHLWINDOW w) { + return w ? getNodeFromTarget(w->layoutTarget()) : nullptr; +} + +int CDwindleAlgorithm::getNodes() { + return m_dwindleNodesData.size(); +} + +SP CDwindleAlgorithm::getFirstNode() { + return m_dwindleNodesData.empty() ? nullptr : m_dwindleNodesData.at(0); +} + +SP CDwindleAlgorithm::getClosestNode(const Vector2D& point) { + SP res = nullptr; + double distClosest = -1; + for (auto& n : m_dwindleNodesData) { + if (n->pTarget && Desktop::View::validMapped(n->pTarget->window())) { + auto distAnother = vecToRectDistanceSquared(point, n->box.pos(), n->box.pos() + n->box.size()); + if (!res || distAnother < distClosest) { + res = n; + distClosest = distAnother; + } + } + } + return res; +} + +SP CDwindleAlgorithm::getMasterNode() { + for (auto& n : m_dwindleNodesData) { + if (!n->pParent) + return n; + } + return nullptr; +} + +std::expected CDwindleAlgorithm::layoutMsg(const std::string_view& sv) { + const auto ARGS = CVarList2(std::string{sv}, 0, ' '); + + const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window()); + + if (ARGS[0] == "togglesplit") { + if (CURRENT_NODE) + toggleSplit(CURRENT_NODE); + } else if (ARGS[0] == "swapsplit") { + if (CURRENT_NODE) + swapSplit(CURRENT_NODE); + } else if (ARGS[0] == "movetoroot") { + auto node = CURRENT_NODE; + if (!ARGS[1].empty()) { + auto w = g_pCompositor->getWindowByRegex(std::string{ARGS[1]}); + if (w) + node = getNodeFromWindow(w); + } + + const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable"; + moveToRoot(node, STABLE); + } else if (ARGS[0] == "preselect") { + auto direction = ARGS[1]; + + if (direction.empty()) { + Log::logger->log(Log::ERR, "Expected direction for preselect"); + return std::unexpected("No direction for preselect"); + } + + switch (direction.front()) { + case 'u': + case 't': { + m_overrideDirection = Math::DIRECTION_UP; + break; + } + case 'd': + case 'b': { + m_overrideDirection = Math::DIRECTION_DOWN; + break; + } + case 'r': { + m_overrideDirection = Math::DIRECTION_RIGHT; + break; + } + case 'l': { + m_overrideDirection = Math::DIRECTION_LEFT; + break; + } + default: { + // any other character resets the focus direction + // needed for the persistent mode + m_overrideDirection = Math::DIRECTION_DEFAULT; + break; + } + } + } + + return {}; +} + +void CDwindleAlgorithm::toggleSplit(SP x) { + if (!x || !x->pParent) + return; + + if (x->pTarget->fullscreenMode() != FSMODE_NONE) + return; + + x->pParent->splitTop = !x->pParent->splitTop; + + x->pParent->recalcSizePosRecursive(); +} + +void CDwindleAlgorithm::swapSplit(SP x) { + if (x->pTarget->fullscreenMode() != FSMODE_NONE) + return; + + std::swap(x->pParent->children[0], x->pParent->children[1]); + + x->pParent->recalcSizePosRecursive(); +} + +void CDwindleAlgorithm::moveToRoot(SP x, bool stable) { + if (!x || !x->pParent) + return; + + if (x->pTarget->fullscreenMode() != FSMODE_NONE) + return; + + // already at root + if (!x->pParent->pParent) + return; + + auto& pNode = x->pParent->children[0] == x ? x->pParent->children[0] : x->pParent->children[1]; + + // instead of [getMasterNodeOnWorkspace], we walk back to root since we need + // to know which children of root is our ancestor + auto pAncestor = x, pRoot = x->pParent.lock(); + while (pRoot->pParent) { + pAncestor = pRoot; + pRoot = pRoot->pParent.lock(); + } + + auto& pSwap = pRoot->children[0] == pAncestor ? pRoot->children[1] : pRoot->children[0]; + std::swap(pNode, pSwap); + std::swap(pNode->pParent, pSwap->pParent); + + // [stable] in that the focused window occupies same side of screen + if (stable) + std::swap(pRoot->children[0], pRoot->children[1]); + + pRoot->recalcSizePosRecursive(); +} diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp new file mode 100644 index 00000000..27c905a4 --- /dev/null +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp @@ -0,0 +1,57 @@ +#include "../../TiledAlgorithm.hpp" + +namespace Layout { + class CAlgorithm; +} + +namespace Layout::Tiled { + struct SDwindleNodeData; + + class CDwindleAlgorithm : public ITiledAlgorithm { + public: + CDwindleAlgorithm() = default; + virtual ~CDwindleAlgorithm() = default; + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); + + virtual SP getNextCandidate(SP old); + + virtual std::expected layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); + + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + private: + std::vector> m_dwindleNodesData; + + struct { + bool started = false; + bool pseudo = false; + bool xExtent = false; + bool yExtent = false; + } m_pseudoDragFlags; + + std::optional m_overrideFocalPoint; // for onWindowCreatedTiling. + + void addTarget(SP target, bool newTarget = true); + void calculateWorkspace(); + SP getNodeFromTarget(SP); + SP getNodeFromWindow(PHLWINDOW w); + int getNodes(); + SP getFirstNode(); + SP getClosestNode(const Vector2D&); + SP getMasterNode(); + + void toggleSplit(SP); + void swapSplit(SP); + void moveToRoot(SP, bool stable = true); + + Math::eDirection m_overrideDirection = Math::DIRECTION_DEFAULT; + }; +}; \ No newline at end of file diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp new file mode 100644 index 00000000..7a6c6768 --- /dev/null +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -0,0 +1,1292 @@ +#include "MasterAlgorithm.hpp" + +#include "../../Algorithm.hpp" +#include "../../../space/Space.hpp" +#include "../../../target/WindowTarget.hpp" + +#include "../../../../config/ConfigValue.hpp" +#include "../../../../config/ConfigManager.hpp" +#include "../../../../desktop/state/FocusState.hpp" +#include "../../../../helpers/Monitor.hpp" +#include "../../../../Compositor.hpp" +#include "../../../../render/Renderer.hpp" + +#include + +using namespace Layout; +using namespace Layout::Tiled; + +struct Layout::Tiled::SMasterNodeData { + bool isMaster = false; + float percMaster = 0.5f; + + WP pTarget; + + Vector2D position; + Vector2D size; + + float percSize = 1.f; // size multiplier for resizing children + + bool ignoreFullscreenChecks = false; + + // + bool operator==(const SMasterNodeData& rhs) const { + return pTarget.lock() == rhs.pTarget.lock(); + } +}; + +void CMasterAlgorithm::newTarget(SP target) { + addTarget(target, true); +} + +void CMasterAlgorithm::movedTarget(SP target, std::optional focalPoint) { + addTarget(target, false); +} + +void CMasterAlgorithm::addTarget(SP target, bool firstMap) { + static auto PNEWONACTIVE = CConfigValue("master:new_on_active"); + static auto PNEWONTOP = CConfigValue("master:new_on_top"); + static auto PNEWSTATUS = CConfigValue("master:new_status"); + + const auto PWORKSPACE = m_parent->space()->workspace(); + const auto PMONITOR = PWORKSPACE->m_monitor; + + bool dragOntoMaster = false; + + if (g_layoutManager->dragController()->wasDraggingWindow()) { + if (const auto n = getClosestNode(g_pInputManager->getMouseCoordsInternal()); n && n->isMaster) + dragOntoMaster = true; + } + + const bool BNEWBEFOREACTIVE = *PNEWONACTIVE == "before"; + const bool BNEWISMASTER = dragOntoMaster || *PNEWSTATUS == "master"; + + const auto PNODE = [&]() -> SP { + if (*PNEWONACTIVE != "none" && !BNEWISMASTER) { + const auto pLastNode = getNodeFromWindow(Desktop::focusState()->window()); + if (pLastNode && !(pLastNode->isMaster && (getMastersNo() == 1 || *PNEWSTATUS == "slave"))) { + auto it = std::ranges::find(m_masterNodesData, pLastNode); + if (!BNEWBEFOREACTIVE) + ++it; + return *m_masterNodesData.emplace(it, makeShared()); + } + } + return *PNEWONTOP ? *m_masterNodesData.emplace(m_masterNodesData.begin(), makeShared()) : m_masterNodesData.emplace_back(makeShared()); + }(); + + PNODE->pTarget = target; + + const auto WINDOWSONWORKSPACE = getNodesNo(); + static auto PMFACT = CConfigValue("master:mfact"); + float lastSplitPercent = *PMFACT; + + auto OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE ? + getNodeFromWindow(Desktop::focusState()->window()) : + getMasterNode(); + + const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); + static auto PDROPATCURSOR = CConfigValue("master:drop_at_cursor"); + eOrientation orientation = getDynamicOrientation(); + const auto NODEIT = std::ranges::find(m_masterNodesData, PNODE); + + bool forceDropAsMaster = false; + // if dragging window to move, drop it at the cursor position instead of bottom/top of stack + if (*PDROPATCURSOR && g_layoutManager->dragController()->mode() == MBIND_MOVE) { + if (WINDOWSONWORKSPACE > 2) { + auto& v = m_masterNodesData; + + const std::size_t srcIndex = static_cast(std::distance(v.begin(), NODEIT)); + + for (std::size_t i = 0; i < v.size(); ++i) { + const CBox box = v[i]->pTarget->position(); + if (!box.containsPoint(MOUSECOORDS)) + continue; + + std::size_t insertIndex = i; + + switch (orientation) { + case ORIENTATION_LEFT: + case ORIENTATION_RIGHT: + if (MOUSECOORDS.y > box.middle().y) + ++insertIndex; // insert after + break; + + case ORIENTATION_TOP: + case ORIENTATION_BOTTOM: + if (MOUSECOORDS.x > box.middle().x) + ++insertIndex; // insert after + break; + + case ORIENTATION_CENTER: break; + + default: UNREACHABLE(); + } + + if (insertIndex > srcIndex) + --insertIndex; + + if (insertIndex == srcIndex) + break; + + auto node = std::move(v[srcIndex]); + v.erase(v.begin() + static_cast(srcIndex)); + v.insert(v.begin() + static_cast(insertIndex), std::move(node)); + + break; + } + } else if (WINDOWSONWORKSPACE == 2) { + // when dropping as the second tiled window in the workspace, + // make it the master only if the cursor is on the master side of the screen + for (auto const& nd : m_masterNodesData) { + if (nd->isMaster) { + const auto MIDDLE = nd->pTarget->position().middle(); + switch (orientation) { + case ORIENTATION_LEFT: + case ORIENTATION_CENTER: + if (MOUSECOORDS.x < MIDDLE.x) + forceDropAsMaster = true; + break; + case ORIENTATION_RIGHT: + if (MOUSECOORDS.x > MIDDLE.x) + forceDropAsMaster = true; + break; + case ORIENTATION_TOP: + if (MOUSECOORDS.y < MIDDLE.y) + forceDropAsMaster = true; + break; + case ORIENTATION_BOTTOM: + if (MOUSECOORDS.y > MIDDLE.y) + forceDropAsMaster = true; + break; + default: UNREACHABLE(); + } + break; + } + } + } + } + + if (BNEWISMASTER // + || WINDOWSONWORKSPACE == 1 // + || (WINDOWSONWORKSPACE > 2 && !firstMap && OPENINGON && OPENINGON->isMaster) // + || forceDropAsMaster // + || (*PNEWSTATUS == "inherit" && OPENINGON && OPENINGON->isMaster && g_layoutManager->dragController()->mode() != MBIND_MOVE)) { + + if (BNEWBEFOREACTIVE) { + for (auto& nd : m_masterNodesData | std::views::reverse) { + if (nd->isMaster) { + nd->isMaster = false; + lastSplitPercent = nd->percMaster; + break; + } + } + } else { + for (auto& nd : m_masterNodesData) { + if (nd->isMaster) { + nd->isMaster = false; + lastSplitPercent = nd->percMaster; + break; + } + } + } + + PNODE->isMaster = true; + PNODE->percMaster = lastSplitPercent; + + // first, check if it isn't too big. + if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PMONITOR->m_size.x * lastSplitPercent || MAXSIZE.y < PMONITOR->m_size.y) { + // we can't continue. make it floating. + m_parent->setFloating(target, true, true); + std::erase(m_masterNodesData, PNODE); + return; + } + } else { + PNODE->isMaster = false; + PNODE->percMaster = lastSplitPercent; + + // first, check if it isn't too big. + if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX); + MAXSIZE.x < PMONITOR->m_size.x * (1 - lastSplitPercent) || MAXSIZE.y < PMONITOR->m_size.y * (1.f / (WINDOWSONWORKSPACE - 1))) { + // we can't continue. make it floating. + m_parent->setFloating(target, true); + std::erase(m_masterNodesData, PNODE); + return; + } + } + + // recalc + calculateWorkspace(); +} + +void CMasterAlgorithm::removeTarget(SP target) { + const auto MASTERSLEFT = getMastersNo(); + static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); + + const auto PNODE = getNodeFromTarget(target); + + if (target->fullscreenMode() != FSMODE_NONE) + g_pCompositor->setWindowFullscreenInternal(target->window(), FSMODE_NONE); + + if (PNODE->isMaster && (MASTERSLEFT <= 1 || *SMALLSPLIT == 1)) { + // find a new master from top of the list + for (auto& nd : m_masterNodesData) { + if (!nd->isMaster) { + nd->isMaster = true; + nd->percMaster = PNODE->percMaster; + break; + } + } + } + + std::erase(m_masterNodesData, PNODE); + + if (getMastersNo() == getNodesNo() && MASTERSLEFT > 1) { + for (auto& nd : m_masterNodesData | std::views::reverse) { + nd->isMaster = false; + break; + } + } + // BUGFIX: correct bug where closing one master in a stack of 2 would leave + // the screen half bare, and make it difficult to select remaining window + if (getNodesNo() == 1) { + for (auto& nd : m_masterNodesData) { + if (!nd->isMaster) { + nd->isMaster = true; + break; + } + } + } + + calculateWorkspace(); +} + +void CMasterAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + const auto PNODE = getNodeFromTarget(target); + + if (!PNODE) + return; + + const auto PMONITOR = m_parent->space()->workspace()->m_monitor; + static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); + static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); + + const auto WORKAREA = PMONITOR->logicalBoxMinusReserved(); + const bool DISPLAYBOTTOM = STICKS(PNODE->position.y + PNODE->size.y, WORKAREA.y + WORKAREA.h); + const bool DISPLAYRIGHT = STICKS(PNODE->position.x + PNODE->size.x, WORKAREA.x + WORKAREA.w); + const bool DISPLAYTOP = STICKS(PNODE->position.y, WORKAREA.y); + const bool DISPLAYLEFT = STICKS(PNODE->position.x, WORKAREA.x); + + const bool LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT; + const bool TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT; + const bool NONE = corner == CORNER_NONE; + + const auto MASTERS = getMastersNo(); + const auto WINDOWS = getNodesNo(); + const auto STACKWINDOWS = WINDOWS - MASTERS; + + eOrientation orientation = getDynamicOrientation(); + bool centered = orientation == ORIENTATION_CENTER && (STACKWINDOWS >= *SLAVECOUNTFORCENTER); + double delta = 0; + + if (getNodesNo() == 1 && !centered) + return; + + m_forceWarps = true; + + switch (orientation) { + case ORIENTATION_LEFT: delta = Δ.x / PMONITOR->m_size.x; break; + case ORIENTATION_RIGHT: delta = -Δ.x / PMONITOR->m_size.x; break; + case ORIENTATION_BOTTOM: delta = -Δ.y / PMONITOR->m_size.y; break; + case ORIENTATION_TOP: delta = Δ.y / PMONITOR->m_size.y; break; + case ORIENTATION_CENTER: + delta = Δ.x / PMONITOR->m_size.x; + if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) { + if (!NONE || !PNODE->isMaster) + delta *= 2; + if ((!PNODE->isMaster && DISPLAYLEFT) || (PNODE->isMaster && LEFT && *PSMARTRESIZING)) + delta = -delta; + } + break; + default: UNREACHABLE(); + } + + for (auto& n : m_masterNodesData) { + if (n->isMaster) + n->percMaster = std::clamp(n->percMaster + delta, 0.05, 0.95); + } + + // check the up/down resize + const bool isStackVertical = orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT || orientation == ORIENTATION_CENTER; + + const auto RESIZEDELTA = isStackVertical ? Δ.y : Δ.x; + + auto nodesInSameColumn = PNODE->isMaster ? MASTERS : STACKWINDOWS; + if (orientation == ORIENTATION_CENTER && !PNODE->isMaster) + nodesInSameColumn = DISPLAYRIGHT ? (nodesInSameColumn + 1) / 2 : nodesInSameColumn / 2; + + const auto SIZE = isStackVertical ? WORKAREA.h / nodesInSameColumn : WORKAREA.w / nodesInSameColumn; + + if (RESIZEDELTA != 0 && nodesInSameColumn > 1) { + if (!*PSMARTRESIZING) { + PNODE->percSize = std::clamp(PNODE->percSize + RESIZEDELTA / SIZE, 0.05, 1.95); + } else { + const auto NODEIT = std::ranges::find(m_masterNodesData, PNODE); + const auto REVNODEIT = std::ranges::find(m_masterNodesData | std::views::reverse, PNODE); + + const float totalSize = isStackVertical ? WORKAREA.h : WORKAREA.w; + const float minSize = totalSize / nodesInSameColumn * 0.2; + const bool resizePrevNodes = isStackVertical ? (TOP || DISPLAYBOTTOM) && !DISPLAYTOP : (LEFT || DISPLAYRIGHT) && !DISPLAYLEFT; + + int nodesLeft = 0; + float sizeLeft = 0; + int nodeCount = 0; + // check the sizes of all the nodes to be resized for later calculation + auto checkNodesLeft = [&sizeLeft, &nodesLeft, orientation, isStackVertical, &nodeCount, PNODE](auto it) { + if (it->isMaster != PNODE->isMaster) + return; + nodeCount++; + if (!it->isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1) + return; + sizeLeft += isStackVertical ? it->size.y : it->size.x; + nodesLeft++; + }; + float resizeDiff; + if (resizePrevNodes) { + std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), checkNodesLeft); + resizeDiff = -RESIZEDELTA; + } else { + std::for_each(std::next(NODEIT), m_masterNodesData.end(), checkNodesLeft); + resizeDiff = RESIZEDELTA; + } + + const float nodeSize = isStackVertical ? PNODE->size.y : PNODE->size.x; + const float maxSizeIncrease = sizeLeft - nodesLeft * minSize; + const float maxSizeDecrease = minSize - nodeSize; + + // leaves enough room for the other nodes + resizeDiff = std::clamp(resizeDiff, maxSizeDecrease, maxSizeIncrease); + PNODE->percSize += resizeDiff / SIZE; + + // resize the other nodes + nodeCount = 0; + auto resizeNodesLeft = [maxSizeIncrease, resizeDiff, minSize, orientation, isStackVertical, SIZE, &nodeCount, nodesLeft, PNODE](auto& it) { + if (it->isMaster != PNODE->isMaster) + return; + nodeCount++; + // if center orientation, only resize when on the same side + if (!it->isMaster && orientation == ORIENTATION_CENTER && nodeCount % 2 == 1) + return; + const float size = isStackVertical ? it->size.y : it->size.x; + const float resizeDeltaForEach = maxSizeIncrease != 0 ? resizeDiff * (size - minSize) / maxSizeIncrease : resizeDiff / nodesLeft; + it->percSize -= resizeDeltaForEach / SIZE; + }; + if (resizePrevNodes) + std::for_each(std::next(REVNODEIT), m_masterNodesData.rend(), resizeNodesLeft); + else + std::for_each(std::next(NODEIT), m_masterNodesData.end(), resizeNodesLeft); + } + } + + recalculate(); + + m_forceWarps = false; +} + +void CMasterAlgorithm::swapTargets(SP a, SP b) { + auto nodeA = getNodeFromTarget(a); + auto nodeB = getNodeFromTarget(b); + + if (nodeA) + nodeA->pTarget = b; + if (nodeB) + nodeB->pTarget = a; +} + +void CMasterAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir); + + if (!t->window()) + return; + + PHLWORKSPACE targetWs; + + if (!PWINDOW2 && t->space() && t->space()->workspace()) { + // try to find a monitor in dir + const auto PMONINDIR = g_pCompositor->getMonitorInDirection(t->space()->workspace()->m_monitor.lock(), dir); + if (PMONINDIR) + targetWs = PMONINDIR->m_activeWorkspace; + } else + targetWs = PWINDOW2->m_workspace; + + if (!targetWs) + return; + + t->window()->setAnimationsToMove(); + + if (t->window()->m_workspace != targetWs) { + t->assignToSpace(targetWs->m_space); + } else if (PWINDOW2) { + // if same monitor, switch windows + g_layoutManager->switchTargets(t, PWINDOW2->layoutTarget()); + if (silent) + Desktop::focusState()->fullWindowFocus(PWINDOW2, Desktop::FOCUS_REASON_KEYBIND); + + recalculate(); + } +} + +void CMasterAlgorithm::recalculate() { + calculateWorkspace(); +} + +std::expected CMasterAlgorithm::layoutMsg(const std::string_view& sv) { + auto switchToWindow = [&](SP target) { + if (!target || !validMapped(target->window())) + return; + + Desktop::focusState()->fullWindowFocus(target->window(), Desktop::FOCUS_REASON_KEYBIND); + g_pCompositor->warpCursorTo(target->position().middle()); + + g_pInputManager->m_forcedFocus = target->window(); + g_pInputManager->simulateMouseMovement(); + g_pInputManager->m_forcedFocus.reset(); + }; + + CVarList2 vars(std::string{sv}, 0, 's'); + + if (vars.size() < 1 || vars[0].empty()) { + Log::logger->log(Log::ERR, "layoutmsg called without params"); + return std::unexpected("layoutmsg without params"); + } + + auto command = vars[0]; + + // swapwithmaster + // first message argument can have the following values: + // * master - keep the focus at the new master + // * child - keep the focus at the new child + // * auto (default) - swap the focus (keep the focus of the previously selected window) + // * ignoremaster - ignore if master is focused + + const auto PWINDOW = Desktop::focusState()->window(); + + if (command == "swapwithmaster") { + if (!PWINDOW) + return std::unexpected("No focused window"); + + if (!isWindowTiled(PWINDOW)) + return std::unexpected("focused window isn't tiled"); + + const auto PMASTER = getMasterNode(); + + if (!PMASTER) + return std::unexpected("no master node"); + + const auto NEWCHILD = PMASTER->pTarget.lock(); + + const bool IGNORE_IF_MASTER = vars.size() >= 2 && std::ranges::any_of(vars, [](const auto& e) { return e == "ignoremaster"; }); + + if (PMASTER->pTarget.lock() != PWINDOW->layoutTarget()) { + const auto& NEWMASTER = PWINDOW->layoutTarget(); + const bool newFocusToChild = vars.size() >= 2 && vars[1] == "child"; + g_layoutManager->switchTargets(NEWMASTER, NEWCHILD); + const auto NEWFOCUS = newFocusToChild ? NEWCHILD : NEWMASTER; + switchToWindow(NEWFOCUS); + } else if (!IGNORE_IF_MASTER) { + for (auto const& n : m_masterNodesData) { + if (!n->isMaster) { + const auto NEWMASTER = n->pTarget.lock(); + g_layoutManager->switchTargets(NEWMASTER, NEWCHILD); + const bool newFocusToMaster = vars.size() >= 2 && vars[1] == "master"; + const auto NEWFOCUS = newFocusToMaster ? NEWMASTER : NEWCHILD; + switchToWindow(NEWFOCUS); + break; + } + } + } + + return {}; + } + // focusmaster + // first message argument can have the following values: + // * master - keep the focus at the new master, even if it was focused before + // * previous - focus window which was previously switched from using `focusmaster previous` command, otherwise fallback to `auto` + // * auto (default) - swap the focus with the first child, if the current focus was master, otherwise focus master + else if (command == "focusmaster") { + if (!PWINDOW) + return std::unexpected("no focused window"); + + const auto PMASTER = getMasterNode(); + + if (!PMASTER) + return std::unexpected("no master"); + + const auto& ARG = vars[1]; // returns empty string if out of bounds + + if (PMASTER->pTarget.lock() != PWINDOW->layoutTarget()) { + switchToWindow(PMASTER->pTarget.lock()); + // save previously focused window (only for `previous` mode) + if (ARG == "previous") + m_workspaceData.focusMasterPrev = PWINDOW->layoutTarget(); + return {}; + } + + const auto focusAuto = [&]() { + // focus first non-master window + for (auto const& n : m_masterNodesData) { + if (!n->isMaster) { + switchToWindow(n->pTarget.lock()); + break; + } + } + }; + + if (ARG == "master") + return {}; + // switch to previously saved window + else if (ARG == "previous") { + const auto PREVWINDOW = m_workspaceData.focusMasterPrev.lock(); + const bool VALID = PREVWINDOW && getNodeFromWindow(PREVWINDOW->window()) && (PWINDOW != PREVWINDOW->window()); + VALID ? switchToWindow(PREVWINDOW) : focusAuto(); + } else + focusAuto(); + } else if (command == "cyclenext") { + if (!PWINDOW) + return std::unexpected("no window"); + + const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; + const auto PNEXTWINDOW = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); + switchToWindow(PNEXTWINDOW); + } else if (command == "cycleprev") { + if (!PWINDOW) + return std::unexpected("no window"); + + const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; + const auto PPREVWINDOW = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP); + switchToWindow(PPREVWINDOW); + } else if (command == "swapnext") { + if (!validMapped(PWINDOW)) + return std::unexpected("no window"); + + if (PWINDOW->layoutTarget()->floating()) { + g_pKeybindManager->m_dispatchers["swapnext"](""); + return {}; + } + + const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; + const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); + + if (PWINDOWTOSWAPWITH) { + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH); + switchToWindow(PWINDOW->layoutTarget()); + } + } else if (command == "swapprev") { + if (!validMapped(PWINDOW)) + return std::unexpected("no window"); + + if (PWINDOW->layoutTarget()->floating()) { + g_pKeybindManager->m_dispatchers["swapnext"]("prev"); + return {}; + } + + const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; + const auto PWINDOWTOSWAPWITH = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP); + + if (PWINDOWTOSWAPWITH) { + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + g_layoutManager->switchTargets(PWINDOW->layoutTarget(), PWINDOWTOSWAPWITH); + switchToWindow(PWINDOW->layoutTarget()); + } + } else if (command == "addmaster") { + if (!validMapped(PWINDOW)) + return std::unexpected("no window"); + + if (PWINDOW->layoutTarget()->floating()) + return std::unexpected("window is floating"); + + const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); + + const auto WINDOWS = getNodesNo(); + const auto MASTERS = getMastersNo(); + static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); + + if (MASTERS + 2 > WINDOWS && *SMALLSPLIT == 0) + return std::unexpected("nothing to do"); + + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + + if (!PNODE || PNODE->isMaster) { + // first non-master node + for (auto& n : m_masterNodesData) { + if (!n->isMaster) { + n->isMaster = true; + break; + } + } + } else { + PNODE->isMaster = true; + } + + calculateWorkspace(); + + } else if (command == "removemaster") { + + if (!validMapped(PWINDOW)) + return std::unexpected("no window"); + + if (PWINDOW->layoutTarget()->floating()) + return std::unexpected("window isnt tiled"); + + const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); + + const auto WINDOWS = getNodesNo(); + const auto MASTERS = getMastersNo(); + + if (WINDOWS < 2 || MASTERS < 2) + return std::unexpected("nothing to do"); + + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + + if (!PNODE || !PNODE->isMaster) { + // first non-master node + for (auto& nd : m_masterNodesData | std::views::reverse) { + if (nd->isMaster) { + nd->isMaster = false; + break; + } + } + } else { + PNODE->isMaster = false; + } + + calculateWorkspace(); + } else if (command == "orientationleft" || command == "orientationright" || command == "orientationtop" || command == "orientationbottom" || command == "orientationcenter") { + if (!PWINDOW) + return std::unexpected("no window"); + + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + + if (command == "orientationleft") + m_workspaceData.orientation = ORIENTATION_LEFT; + else if (command == "orientationright") + m_workspaceData.orientation = ORIENTATION_RIGHT; + else if (command == "orientationtop") + m_workspaceData.orientation = ORIENTATION_TOP; + else if (command == "orientationbottom") + m_workspaceData.orientation = ORIENTATION_BOTTOM; + else if (command == "orientationcenter") + m_workspaceData.orientation = ORIENTATION_CENTER; + + calculateWorkspace(); + } else if (command == "orientationnext") { + runOrientationCycle(nullptr, 1); + } else if (command == "orientationprev") { + runOrientationCycle(nullptr, -1); + } else if (command == "orientationcycle") { + runOrientationCycle(&vars, 1); + } else if (command == "mfact") { + + if (!PWINDOW) + return std::unexpected("no window"); + + const bool exact = vars[1] == "exact"; + + float ratio = 0.F; + + try { + ratio = std::stof(std::string{exact ? vars[2] : vars[1]}); + } catch (...) { return std::unexpected("bad ratio"); } + + const auto PNODE = getNodeFromWindow(PWINDOW); + + const auto PMASTER = getMasterNode(); + + float newRatio = exact ? ratio : PMASTER->percMaster + ratio; + PMASTER->percMaster = std::clamp(newRatio, 0.05f, 0.95f); + + recalculate(); + } else if (command == "rollnext") { + const auto PNODE = getNodeFromWindow(PWINDOW); + + if (!PNODE) + return std::unexpected("window couldnt be found"); + + const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode(); + if (!OLDMASTER) + return std::unexpected("no old master"); + + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + for (auto& nd : m_masterNodesData) { + if (!nd->isMaster) { + const auto newMaster = nd; + newMaster->isMaster = true; + + auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); + + if (newMasterIt < oldMasterIt) + std::ranges::rotate(newMasterIt, std::next(newMasterIt), oldMasterIt); + else if (newMasterIt > oldMasterIt) + std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); + + switchToWindow(newMaster->pTarget.lock()); + OLDMASTER->isMaster = false; + + oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + if (oldMasterIt != m_masterNodesData.end()) + std::ranges::rotate(oldMasterIt, std::next(oldMasterIt), m_masterNodesData.end()); + + break; + } + } + + calculateWorkspace(); + } else if (command == "rollprev") { + const auto PNODE = getNodeFromWindow(PWINDOW); + + if (!PNODE) + return std::unexpected("window couldnt be found"); + + const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode(); + if (!OLDMASTER) + return std::unexpected("no old master"); + + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + for (auto& nd : m_masterNodesData | std::views::reverse) { + if (!nd->isMaster) { + const auto newMaster = nd; + newMaster->isMaster = true; + + auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); + + if (newMasterIt < oldMasterIt) + std::ranges::rotate(newMasterIt, std::next(newMasterIt), oldMasterIt); + else if (newMasterIt > oldMasterIt) + std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); + + switchToWindow(newMaster->pTarget.lock()); + OLDMASTER->isMaster = false; + + oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + if (oldMasterIt != m_masterNodesData.begin()) + std::ranges::rotate(m_masterNodesData.begin(), oldMasterIt, std::next(oldMasterIt)); + + break; + } + } + + calculateWorkspace(); + } + + return {}; +} + +std::optional CMasterAlgorithm::predictSizeForNewTarget() { + static auto PNEWSTATUS = CConfigValue("master:new_status"); + + const auto MONITOR = m_parent->space()->workspace()->m_monitor; + + if (!MONITOR) + return std::nullopt; + + const int NODES = getNodesNo(); + + if (NODES <= 0) + return Desktop::focusState()->monitor()->m_size; + + const auto MASTER = getMasterNode(); + if (!MASTER) // wtf + return std::nullopt; + + if (*PNEWSTATUS == "master") { + return MASTER->size; + } else { + const auto SLAVES = NODES - getMastersNo(); + + // TODO: make this better + if (SLAVES == 0) + return Vector2D{MONITOR->m_size.x / 2.F, MONITOR->m_size.y}; + else + return Vector2D{MONITOR->m_size.x - MASTER->size.x, MONITOR->m_size.y / (SLAVES + 1)}; + } + + return std::nullopt; +} + +void CMasterAlgorithm::buildOrientationCycleVectorFromVars(std::vector& cycle, Hyprutils::String::CVarList2* vars) { + for (size_t i = 1; i < vars->size(); ++i) { + if ((*vars)[i] == "top") { + cycle.emplace_back(ORIENTATION_TOP); + } else if ((*vars)[i] == "right") { + cycle.emplace_back(ORIENTATION_RIGHT); + } else if ((*vars)[i] == "bottom") { + cycle.emplace_back(ORIENTATION_BOTTOM); + } else if ((*vars)[i] == "left") { + cycle.emplace_back(ORIENTATION_LEFT); + } else if ((*vars)[i] == "center") { + cycle.emplace_back(ORIENTATION_CENTER); + } + } +} + +void CMasterAlgorithm::buildOrientationCycleVectorFromEOperation(std::vector& cycle) { + for (int i = 0; i <= ORIENTATION_CENTER; ++i) { + cycle.push_back(sc(i)); + } +} + +void CMasterAlgorithm::runOrientationCycle(Hyprutils::String::CVarList2* vars, int next) { + std::vector cycle; + if (vars != nullptr) + buildOrientationCycleVectorFromVars(cycle, vars); + + if (cycle.empty()) + buildOrientationCycleVectorFromEOperation(cycle); + + const auto PWINDOW = Desktop::focusState()->window(); + + if (!PWINDOW) + return; + + g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); + + int nextOrPrev = 0; + for (size_t i = 0; i < cycle.size(); ++i) { + if (m_workspaceData.orientation == cycle[i]) { + nextOrPrev = i + next; + break; + } + } + + if (nextOrPrev >= sc(cycle.size())) + nextOrPrev = nextOrPrev % sc(cycle.size()); + else if (nextOrPrev < 0) + nextOrPrev = cycle.size() + (nextOrPrev % sc(cycle.size())); + + m_workspaceData.orientation = cycle.at(nextOrPrev); + calculateWorkspace(); +} + +eOrientation CMasterAlgorithm::getDynamicOrientation() { + const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(m_parent->space()->workspace()); + std::string orientationString; + if (WORKSPACERULE.layoutopts.contains("orientation")) + orientationString = WORKSPACERULE.layoutopts.at("orientation"); + + eOrientation orientation = m_workspaceData.orientation; + // override if workspace rule is set + if (!orientationString.empty()) { + if (orientationString == "top") + orientation = ORIENTATION_TOP; + else if (orientationString == "right") + orientation = ORIENTATION_RIGHT; + else if (orientationString == "bottom") + orientation = ORIENTATION_BOTTOM; + else if (orientationString == "center") + orientation = ORIENTATION_CENTER; + else + orientation = ORIENTATION_LEFT; + } + + return orientation; +} + +int CMasterAlgorithm::getNodesNo() { + return m_masterNodesData.size(); +} + +SP CMasterAlgorithm::getNodeFromWindow(PHLWINDOW x) { + return x ? getNodeFromTarget(x->layoutTarget()) : nullptr; +} + +SP CMasterAlgorithm::getNodeFromTarget(SP x) { + for (const auto& n : m_masterNodesData) { + if (n->pTarget == x) + return n; + } + + return nullptr; +} + +SP CMasterAlgorithm::getMasterNode() { + for (const auto& n : m_masterNodesData) { + if (n->isMaster) + return n; + } + + return nullptr; +} + +void CMasterAlgorithm::calculateWorkspace() { + const auto PMASTERNODE = getMasterNode(); + + if (!PMASTERNODE) + return; + + Hyprutils::Utils::CScopeGuard x([this] { + g_pHyprRenderer->damageMonitor(m_parent->space()->workspace()->m_monitor.lock()); + + if (!m_forceWarps) + return; + + for (const auto& n : m_masterNodesData) { + n->pTarget->warpPositionSize(); + } + }); + + eOrientation orientation = getDynamicOrientation(); + bool centerMasterWindow = false; + static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); + static auto CMFALLBACK = CConfigValue("master:center_master_fallback"); + static auto PIGNORERESERVED = CConfigValue("master:center_ignores_reserved"); + static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); + + const auto MASTERS = getMastersNo(); + const auto WINDOWS = getNodesNo(); + const auto STACKWINDOWS = WINDOWS - MASTERS; + const auto WORKAREA = m_parent->space()->workArea(); + const auto PMONITOR = m_parent->space()->workspace()->m_monitor; + const auto UNRESERVED_WIDTH = WORKAREA.width + PMONITOR->m_reservedArea.left() + PMONITOR->m_reservedArea.right(); + + if (orientation == ORIENTATION_CENTER) { + if (STACKWINDOWS >= *SLAVECOUNTFORCENTER) + centerMasterWindow = true; + else { + if (*CMFALLBACK == "left") + orientation = ORIENTATION_LEFT; + else if (*CMFALLBACK == "right") + orientation = ORIENTATION_RIGHT; + else if (*CMFALLBACK == "top") + orientation = ORIENTATION_TOP; + else if (*CMFALLBACK == "bottom") + orientation = ORIENTATION_BOTTOM; + else + orientation = ORIENTATION_LEFT; + } + } + + const float totalSize = (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) ? WORKAREA.w : WORKAREA.h; + const float masterAverageSize = totalSize / MASTERS; + const float slaveAverageSize = totalSize / STACKWINDOWS; + float masterAccumulatedSize = 0; + float slaveAccumulatedSize = 0; + + if (*PSMARTRESIZING) { + // check the total width and height so that later + // if larger/smaller than screen size them down/up + for (auto const& nd : m_masterNodesData) { + if (nd->isMaster) + masterAccumulatedSize += totalSize / MASTERS * nd->percSize; + else + slaveAccumulatedSize += totalSize / STACKWINDOWS * nd->percSize; + } + } + + // compute placement of master window(s) + if (WINDOWS == 1 && !centerMasterWindow) { + static auto PALWAYSKEEPPOSITION = CConfigValue("master:always_keep_position"); + if (*PALWAYSKEEPPOSITION) { + const float WIDTH = WORKAREA.w * PMASTERNODE->percMaster; + float nextX = 0; + + if (orientation == ORIENTATION_RIGHT) + nextX = WORKAREA.w - WIDTH; + else if (orientation == ORIENTATION_CENTER) + nextX = (WORKAREA.w - WIDTH) / 2; + + PMASTERNODE->size = Vector2D(WIDTH, WORKAREA.h); + PMASTERNODE->position = WORKAREA.pos() + Vector2D(nextX, 0.0); + } else { + PMASTERNODE->size = WORKAREA.size(); + PMASTERNODE->position = WORKAREA.pos(); + } + + PMASTERNODE->pTarget->setPositionGlobal({PMASTERNODE->position, PMASTERNODE->size}); + return; + } else if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) { + const float HEIGHT = STACKWINDOWS != 0 ? WORKAREA.h * PMASTERNODE->percMaster : WORKAREA.h; + float widthLeft = WORKAREA.w; + int mastersLeft = MASTERS; + float nextX = 0; + float nextY = 0; + + if (orientation == ORIENTATION_BOTTOM) + nextY = WORKAREA.h - HEIGHT; + + for (auto& nd : m_masterNodesData) { + if (!nd->isMaster) + continue; + + float WIDTH = mastersLeft > 1 ? widthLeft / mastersLeft * nd->percSize : widthLeft; + if (WIDTH > widthLeft * 0.9f && mastersLeft > 1) + WIDTH = widthLeft * 0.9f; + + if (*PSMARTRESIZING) { + nd->percSize *= WORKAREA.w / masterAccumulatedSize; + WIDTH = masterAverageSize * nd->percSize; + } + + nd->size = Vector2D(WIDTH, HEIGHT); + nd->position = WORKAREA.pos() + Vector2D(nextX, nextY); + nd->pTarget->setPositionGlobal({nd->position, nd->size}); + + mastersLeft--; + widthLeft -= WIDTH; + nextX += WIDTH; + } + } else { // orientation left, right or center + const float TOTAL_WIDTH = *PIGNORERESERVED && centerMasterWindow ? UNRESERVED_WIDTH : WORKAREA.w; + float WIDTH = TOTAL_WIDTH; + float heightLeft = WORKAREA.h; + int mastersLeft = MASTERS; + float nextX = 0; + float nextY = 0; + + if (STACKWINDOWS > 0 || centerMasterWindow) + WIDTH *= PMASTERNODE->percMaster; + + if (orientation == ORIENTATION_RIGHT) + nextX = WORKAREA.w - WIDTH; + else if (centerMasterWindow) + nextX += (TOTAL_WIDTH - WIDTH) / 2; + + for (auto& nd : m_masterNodesData) { + if (!nd->isMaster) + continue; + + float HEIGHT = mastersLeft > 1 ? heightLeft / mastersLeft * nd->percSize : heightLeft; + if (HEIGHT > heightLeft * 0.9f && mastersLeft > 1) + HEIGHT = heightLeft * 0.9f; + + if (*PSMARTRESIZING) { + nd->percSize *= WORKAREA.h / masterAccumulatedSize; + HEIGHT = masterAverageSize * nd->percSize; + } + + nd->size = Vector2D(WIDTH, HEIGHT); + nd->position = (*PIGNORERESERVED && centerMasterWindow ? WORKAREA.pos() - Vector2D(PMONITOR->m_reservedArea.left(), 0.0) : WORKAREA.pos()) + Vector2D(nextX, nextY); + nd->pTarget->setPositionGlobal({nd->position, nd->size}); + + mastersLeft--; + heightLeft -= HEIGHT; + nextY += HEIGHT; + } + } + + if (STACKWINDOWS == 0) + return; + + // compute placement of slave window(s) + int slavesLeft = STACKWINDOWS; + if (orientation == ORIENTATION_TOP || orientation == ORIENTATION_BOTTOM) { + const float HEIGHT = WORKAREA.h - PMASTERNODE->size.y; + float widthLeft = WORKAREA.w; + float nextX = 0; + float nextY = 0; + + if (orientation == ORIENTATION_TOP) + nextY = PMASTERNODE->size.y; + + for (auto& nd : m_masterNodesData) { + if (nd->isMaster) + continue; + + float WIDTH = slavesLeft > 1 ? widthLeft / slavesLeft * nd->percSize : widthLeft; + if (WIDTH > widthLeft * 0.9f && slavesLeft > 1) + WIDTH = widthLeft * 0.9f; + + if (*PSMARTRESIZING) { + nd->percSize *= WORKAREA.w / slaveAccumulatedSize; + WIDTH = slaveAverageSize * nd->percSize; + } + + nd->size = Vector2D(WIDTH, HEIGHT); + nd->position = WORKAREA.pos() + Vector2D(nextX, nextY); + nd->pTarget->setPositionGlobal({nd->position, nd->size}); + + slavesLeft--; + widthLeft -= WIDTH; + nextX += WIDTH; + } + } else if (orientation == ORIENTATION_LEFT || orientation == ORIENTATION_RIGHT) { + const float WIDTH = WORKAREA.w - PMASTERNODE->size.x; + float heightLeft = WORKAREA.h; + float nextY = 0; + float nextX = 0; + + if (orientation == ORIENTATION_LEFT) + nextX = PMASTERNODE->size.x; + + for (auto& nd : m_masterNodesData) { + if (nd->isMaster) + continue; + + float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd->percSize : heightLeft; + if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1) + HEIGHT = heightLeft * 0.9f; + + if (*PSMARTRESIZING) { + nd->percSize *= WORKAREA.h / slaveAccumulatedSize; + HEIGHT = slaveAverageSize * nd->percSize; + } + + nd->size = Vector2D(WIDTH, HEIGHT); + nd->position = WORKAREA.pos() + Vector2D(nextX, nextY); + nd->pTarget->setPositionGlobal({nd->position, nd->size}); + + slavesLeft--; + heightLeft -= HEIGHT; + nextY += HEIGHT; + } + } else { // slaves for centered master window(s) + const float WIDTH = ((*PIGNORERESERVED ? UNRESERVED_WIDTH : WORKAREA.w) - PMASTERNODE->size.x) / 2.0; + float heightLeft = 0; + float heightLeftL = WORKAREA.h; + float heightLeftR = WORKAREA.h; + float nextX = 0; + float nextY = 0; + float nextYL = 0; + float nextYR = 0; + bool onRight = *CMFALLBACK == "right"; + int slavesLeftL = 1 + (slavesLeft - 1) / 2; + int slavesLeftR = slavesLeft - slavesLeftL; + + if (onRight) { + slavesLeftR = 1 + (slavesLeft - 1) / 2; + slavesLeftL = slavesLeft - slavesLeftR; + } + + const float slaveAverageHeightL = WORKAREA.h / slavesLeftL; + const float slaveAverageHeightR = WORKAREA.h / slavesLeftR; + float slaveAccumulatedHeightL = 0; + float slaveAccumulatedHeightR = 0; + + if (*PSMARTRESIZING) { + for (auto const& nd : m_masterNodesData) { + if (nd->isMaster) + continue; + + if (onRight) + slaveAccumulatedHeightR += slaveAverageHeightR * nd->percSize; + else + slaveAccumulatedHeightL += slaveAverageHeightL * nd->percSize; + + onRight = !onRight; + } + + onRight = *CMFALLBACK == "right"; + } + + for (auto& nd : m_masterNodesData) { + if (nd->isMaster) + continue; + + if (onRight) { + nextX = WIDTH + PMASTERNODE->size.x - (*PIGNORERESERVED ? PMONITOR->m_reservedArea.left() : 0); + nextY = nextYR; + heightLeft = heightLeftR; + slavesLeft = slavesLeftR; + } else { + nextX = 0; + nextY = nextYL; + heightLeft = heightLeftL; + slavesLeft = slavesLeftL; + } + + float HEIGHT = slavesLeft > 1 ? heightLeft / slavesLeft * nd->percSize : heightLeft; + if (HEIGHT > heightLeft * 0.9f && slavesLeft > 1) + HEIGHT = heightLeft * 0.9f; + + if (*PSMARTRESIZING) { + if (onRight) { + nd->percSize *= WORKAREA.h / slaveAccumulatedHeightR; + HEIGHT = slaveAverageHeightR * nd->percSize; + } else { + nd->percSize *= WORKAREA.h / slaveAccumulatedHeightL; + HEIGHT = slaveAverageHeightL * nd->percSize; + } + } + + nd->size = Vector2D(*PIGNORERESERVED ? (WIDTH - (onRight ? PMONITOR->m_reservedArea.right() : PMONITOR->m_reservedArea.left())) : WIDTH, HEIGHT); + nd->position = WORKAREA.pos() + Vector2D(nextX, nextY); + nd->pTarget->setPositionGlobal({nd->position, nd->size}); + + if (onRight) { + heightLeftR -= HEIGHT; + nextYR += HEIGHT; + slavesLeftR--; + } else { + heightLeftL -= HEIGHT; + nextYL += HEIGHT; + slavesLeftL--; + } + + onRight = !onRight; + } + } +} + +SP CMasterAlgorithm::getNextCandidate(SP old) { + const auto MIDDLE = old->position().middle(); + + if (const auto NODE = getClosestNode(MIDDLE); NODE) + return NODE->pTarget.lock(); + + if (const auto NODE = getMasterNode(); NODE) + return NODE->pTarget.lock(); + + return nullptr; +} + +SP CMasterAlgorithm::getNextTarget(SP t, bool next, bool loop) { + if (t->floating()) + return nullptr; + + const auto PNODE = getNodeFromTarget(t); + + auto nodes = m_masterNodesData; + if (!next) + std::ranges::reverse(nodes); + + const auto NODEIT = std::ranges::find(nodes, PNODE); + + const bool ISMASTER = PNODE->isMaster; + + auto CANDIDATE = std::find_if(NODEIT, nodes.end(), [&](const auto& other) { return other != PNODE && ISMASTER == other->isMaster; }); + if (CANDIDATE == nodes.end()) + CANDIDATE = std::ranges::find_if(nodes, [&](const auto& other) { return other != PNODE && ISMASTER != other->isMaster; }); + + if (CANDIDATE != nodes.end() && !loop) { + if ((*CANDIDATE)->isMaster && next) + return nullptr; + if (!(*CANDIDATE)->isMaster && ISMASTER && !next) + return nullptr; + } + + return CANDIDATE == nodes.end() ? nullptr : (*CANDIDATE)->pTarget.lock(); +} + +int CMasterAlgorithm::getMastersNo() { + return std::ranges::count_if(m_masterNodesData, [](const auto& n) { return n->isMaster; }); +} + +bool CMasterAlgorithm::isWindowTiled(PHLWINDOW x) { + return x && !x->layoutTarget()->floating(); +} + +SP CMasterAlgorithm::getClosestNode(const Vector2D& point) { + SP res = nullptr; + double distClosest = -1; + for (auto& n : m_masterNodesData) { + if (n->pTarget && Desktop::View::validMapped(n->pTarget->window())) { + auto distAnother = vecToRectDistanceSquared(point, n->position, n->position + n->size); + if (!res || distAnother < distClosest) { + res = n; + distClosest = distAnother; + } + } + } + return res; +} diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp new file mode 100644 index 00000000..4524587f --- /dev/null +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp @@ -0,0 +1,75 @@ +#include "../../TiledAlgorithm.hpp" + +#include + +namespace Layout { + class CAlgorithm; +} + +namespace Layout::Tiled { + struct SMasterNodeData; + + //orientation determines which side of the screen the master area resides + enum eOrientation : uint8_t { + ORIENTATION_LEFT = 0, + ORIENTATION_TOP, + ORIENTATION_RIGHT, + ORIENTATION_BOTTOM, + ORIENTATION_CENTER + }; + + struct SMasterWorkspaceData { + WORKSPACEID workspaceID = WORKSPACE_INVALID; + eOrientation orientation = ORIENTATION_LEFT; + // Previously focused non-master window when `focusmaster previous` command was issued + WP focusMasterPrev; + + // + bool operator==(const SMasterWorkspaceData& rhs) const { + return workspaceID == rhs.workspaceID; + } + }; + + class CMasterAlgorithm : public ITiledAlgorithm { + public: + CMasterAlgorithm() = default; + virtual ~CMasterAlgorithm() = default; + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); + + virtual SP getNextCandidate(SP old); + + virtual std::expected layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); + + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + private: + std::vector> m_masterNodesData; + SMasterWorkspaceData m_workspaceData; + + void addTarget(SP target, bool firstMap); + + bool m_forceWarps = false; + + void buildOrientationCycleVectorFromVars(std::vector& cycle, Hyprutils::String::CVarList2* vars); + void buildOrientationCycleVectorFromEOperation(std::vector& cycle); + void runOrientationCycle(Hyprutils::String::CVarList2* vars, int next); + eOrientation getDynamicOrientation(); + int getNodesNo(); + SP getNodeFromWindow(PHLWINDOW); + SP getNodeFromTarget(SP); + SP getMasterNode(); + SP getClosestNode(const Vector2D&); + void calculateWorkspace(); + SP getNextTarget(SP, bool, bool); + int getMastersNo(); + bool isWindowTiled(PHLWINDOW); + }; +}; \ No newline at end of file diff --git a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp new file mode 100644 index 00000000..65533e71 --- /dev/null +++ b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp @@ -0,0 +1,274 @@ +#include "MonocleAlgorithm.hpp" + +#include "../../Algorithm.hpp" +#include "../../../space/Space.hpp" +#include "../../../target/WindowTarget.hpp" +#include "../../../LayoutManager.hpp" + +#include "../../../../config/ConfigValue.hpp" +#include "../../../../desktop/state/FocusState.hpp" +#include "../../../../desktop/history/WindowHistoryTracker.hpp" +#include "../../../../helpers/Monitor.hpp" +#include "../../../../Compositor.hpp" + +#include +#include +#include + +using namespace Hyprutils::String; +using namespace Hyprutils::Utils; +using namespace Layout; +using namespace Layout::Tiled; + +CMonocleAlgorithm::CMonocleAlgorithm() { + // hook into focus changes to bring focused window to front + m_focusCallback = g_pHookSystem->hookDynamic("activeWindow", [this](void* hk, SCallbackInfo& info, std::any param) { + const auto PWINDOW = std::any_cast(param).window; + + if (!PWINDOW) + return; + + if (!PWINDOW->m_workspace->isVisible()) + return; + + const auto TARGET = PWINDOW->layoutTarget(); + if (!TARGET) + return; + + focusTargetUpdate(TARGET); + }); +} + +CMonocleAlgorithm::~CMonocleAlgorithm() { + // unhide all windows before destruction + for (const auto& data : m_targetDatas) { + const auto TARGET = data->target.lock(); + if (!TARGET) + continue; + + const auto WINDOW = TARGET->window(); + if (WINDOW) + WINDOW->setHidden(false); + } + + m_focusCallback.reset(); +} + +SP CMonocleAlgorithm::dataFor(SP t) { + for (auto& data : m_targetDatas) { + if (data->target.lock() == t) + return data; + } + return nullptr; +} + +void CMonocleAlgorithm::newTarget(SP target) { + const auto DATA = m_targetDatas.emplace_back(makeShared(target)); + + m_currentVisibleIndex = m_targetDatas.size() - 1; + + recalculate(); +} + +void CMonocleAlgorithm::movedTarget(SP target, std::optional focalPoint) { + newTarget(target); +} + +void CMonocleAlgorithm::removeTarget(SP target) { + auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; }); + + if (it == m_targetDatas.end()) + return; + + // unhide window when removing from monocle layout + const auto WINDOW = target->window(); + if (WINDOW) + WINDOW->setHidden(false); + + const auto INDEX = std::distance(m_targetDatas.begin(), it); + m_targetDatas.erase(it); + + if (m_targetDatas.empty()) { + m_currentVisibleIndex = 0; + return; + } + + // try to use the last window in history if we can + for (const auto& historyWindow : Desktop::History::windowTracker()->historyForWorkspace(m_parent->space()->workspace()) | std::views::reverse) { + auto it = std::ranges::find_if(m_targetDatas, [&historyWindow](const auto& d) { return d->target == historyWindow->layoutTarget(); }); + + if (it == m_targetDatas.end()) + continue; + + // we found a historical target, use that first + m_currentVisibleIndex = std::distance(m_targetDatas.begin(), it); + + recalculate(); + + return; + } + + // if we didn't find history, fall back to last + + if (m_currentVisibleIndex >= (int)m_targetDatas.size()) + m_currentVisibleIndex = m_targetDatas.size() - 1; + else if (INDEX <= m_currentVisibleIndex && m_currentVisibleIndex > 0) + m_currentVisibleIndex--; + + recalculate(); +} + +void CMonocleAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + // monocle layout doesn't support manual resizing, all windows are fullscreen +} + +void CMonocleAlgorithm::recalculate() { + if (m_targetDatas.empty()) + return; + + const auto WORK_AREA = m_parent->space()->workArea(); + + for (size_t i = 0; i < m_targetDatas.size(); ++i) { + const auto& DATA = m_targetDatas[i]; + const auto TARGET = DATA->target.lock(); + + if (!TARGET) + continue; + + const auto WINDOW = TARGET->window(); + if (!WINDOW) + continue; + + DATA->layoutBox = WORK_AREA; + TARGET->setPositionGlobal(WORK_AREA); + + const bool SHOULD_BE_VISIBLE = ((int)i == m_currentVisibleIndex); + WINDOW->setHidden(!SHOULD_BE_VISIBLE); + } +} + +SP CMonocleAlgorithm::getNextCandidate(SP old) { + if (m_targetDatas.empty()) + return nullptr; + + auto it = std::ranges::find_if(m_targetDatas, [old](const auto& data) { return data->target.lock() == old; }); + + if (it == m_targetDatas.end()) { + if (m_currentVisibleIndex >= 0 && m_currentVisibleIndex < (int)m_targetDatas.size()) + return m_targetDatas[m_currentVisibleIndex]->target.lock(); + return nullptr; + } + + auto next = std::next(it); + if (next == m_targetDatas.end()) + next = m_targetDatas.begin(); + + return next->get()->target.lock(); +} + +std::expected CMonocleAlgorithm::layoutMsg(const std::string_view& sv) { + CVarList2 vars(std::string{sv}, 0, 's'); + + if (vars.size() < 1) + return std::unexpected("layoutmsg requires at least 1 argument"); + + const auto COMMAND = vars[0]; + + if (COMMAND == "cyclenext") { + cycleNext(); + return {}; + } else if (COMMAND == "cycleprev") { + cyclePrev(); + return {}; + } + + return std::unexpected(std::format("Unknown monocle layoutmsg: {}", COMMAND)); +} + +std::optional CMonocleAlgorithm::predictSizeForNewTarget() { + const auto WORK_AREA = m_parent->space()->workArea(); + return WORK_AREA.size(); +} + +void CMonocleAlgorithm::swapTargets(SP a, SP b) { + auto nodeA = dataFor(a); + auto nodeB = dataFor(b); + + if (nodeA) + nodeA->target = b; + if (nodeB) + nodeB->target = a; + + recalculate(); +} + +void CMonocleAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + // try to find a monitor in the specified direction, thats the logical thing + if (!t || !t->space() || !t->space()->workspace()) + return; + + const auto PMONINDIR = g_pCompositor->getMonitorInDirection(t->space()->workspace()->m_monitor.lock(), dir); + + // if we found a monitor, move the window there + if (PMONINDIR && PMONINDIR != t->space()->workspace()->m_monitor.lock()) { + const auto TARGETWS = PMONINDIR->m_activeWorkspace; + + if (t->window()) + t->window()->setAnimationsToMove(); + + t->assignToSpace(TARGETWS->m_space); + } +} + +void CMonocleAlgorithm::cycleNext() { + if (m_targetDatas.empty()) + return; + + m_currentVisibleIndex = (m_currentVisibleIndex + 1) % m_targetDatas.size(); + updateVisible(); +} + +void CMonocleAlgorithm::cyclePrev() { + if (m_targetDatas.empty()) + return; + + m_currentVisibleIndex--; + if (m_currentVisibleIndex < 0) + m_currentVisibleIndex = m_targetDatas.size() - 1; + updateVisible(); +} + +void CMonocleAlgorithm::focusTargetUpdate(SP target) { + auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; }); + + if (it == m_targetDatas.end()) + return; + + const auto NEW_INDEX = std::distance(m_targetDatas.begin(), it); + + if (m_currentVisibleIndex != NEW_INDEX) { + m_currentVisibleIndex = NEW_INDEX; + updateVisible(); + } +} + +void CMonocleAlgorithm::updateVisible() { + recalculate(); + + const auto VISIBLE_TARGET = getVisibleTarget(); + if (!VISIBLE_TARGET) + return; + + const auto WINDOW = VISIBLE_TARGET->window(); + if (!WINDOW) + return; + + Desktop::focusState()->fullWindowFocus(WINDOW, Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); +} + +SP CMonocleAlgorithm::getVisibleTarget() { + if (m_currentVisibleIndex < 0 || m_currentVisibleIndex >= (int)m_targetDatas.size()) + return nullptr; + + return m_targetDatas[m_currentVisibleIndex]->target.lock(); +} diff --git a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp new file mode 100644 index 00000000..e409b885 --- /dev/null +++ b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "../../TiledAlgorithm.hpp" +#include "../../../../managers/HookSystemManager.hpp" + +#include + +namespace Layout::Tiled { + + struct SMonocleTargetData { + SMonocleTargetData(SP t) : target(t) { + ; + } + + WP target; + CBox layoutBox; + }; + + class CMonocleAlgorithm : public ITiledAlgorithm { + public: + CMonocleAlgorithm(); + virtual ~CMonocleAlgorithm(); + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); + + virtual SP getNextCandidate(SP old); + + virtual std::expected layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); + + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + private: + std::vector> m_targetDatas; + SP m_focusCallback; + + int m_currentVisibleIndex = 0; + + SP dataFor(SP t); + void cycleNext(); + void cyclePrev(); + void focusTargetUpdate(SP target); + void updateVisible(); + SP getVisibleTarget(); + }; +}; diff --git a/src/layout/algorithm/tiled/scrolling/ScrollTapeController.cpp b/src/layout/algorithm/tiled/scrolling/ScrollTapeController.cpp new file mode 100644 index 00000000..63b98717 --- /dev/null +++ b/src/layout/algorithm/tiled/scrolling/ScrollTapeController.cpp @@ -0,0 +1,293 @@ +#include "ScrollTapeController.hpp" +#include "ScrollingAlgorithm.hpp" +#include +#include + +using namespace Layout::Tiled; + +CScrollTapeController::CScrollTapeController(eScrollDirection direction) : m_direction(direction) { + ; +} + +void CScrollTapeController::setDirection(eScrollDirection dir) { + m_direction = dir; +} + +eScrollDirection CScrollTapeController::getDirection() const { + return m_direction; +} + +bool CScrollTapeController::isPrimaryHorizontal() const { + return m_direction == SCROLL_DIR_RIGHT || m_direction == SCROLL_DIR_LEFT; +} + +bool CScrollTapeController::isReversed() const { + return m_direction == SCROLL_DIR_LEFT || m_direction == SCROLL_DIR_UP; +} + +size_t CScrollTapeController::stripCount() const { + return m_strips.size(); +} + +SStripData& CScrollTapeController::getStrip(size_t index) { + return m_strips[index]; +} + +const SStripData& CScrollTapeController::getStrip(size_t index) const { + return m_strips[index]; +} + +void CScrollTapeController::setOffset(double offset) { + m_offset = offset; +} + +double CScrollTapeController::getOffset() const { + return m_offset; +} + +void CScrollTapeController::adjustOffset(double delta) { + m_offset += delta; +} + +size_t CScrollTapeController::addStrip(float size) { + m_strips.emplace_back(); + m_strips.back().size = size; + return m_strips.size() - 1; +} + +void CScrollTapeController::insertStrip(size_t afterIndex, float size) { + if (afterIndex >= m_strips.size()) { + addStrip(size); + return; + } + + SStripData newStrip; + newStrip.size = size; + m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip); +} + +void CScrollTapeController::removeStrip(size_t index) { + if (index < m_strips.size()) + m_strips.erase(m_strips.begin() + index); +} + +double CScrollTapeController::getPrimary(const Vector2D& v) const { + return isPrimaryHorizontal() ? v.x : v.y; +} + +double CScrollTapeController::getSecondary(const Vector2D& v) const { + return isPrimaryHorizontal() ? v.y : v.x; +} + +void CScrollTapeController::setPrimary(Vector2D& v, double val) const { + if (isPrimaryHorizontal()) + v.x = val; + else + v.y = val; +} + +void CScrollTapeController::setSecondary(Vector2D& v, double val) const { + if (isPrimaryHorizontal()) + v.y = val; + else + v.x = val; +} + +Vector2D CScrollTapeController::makeVector(double primary, double secondary) const { + if (isPrimaryHorizontal()) + return {primary, secondary}; + else + return {secondary, primary}; +} + +double CScrollTapeController::calculateMaxExtent(const CBox& usableArea, bool fullscreenOnOne) const { + if (m_strips.empty()) + return 0.0; + + if (fullscreenOnOne && m_strips.size() == 1) + return getPrimary(usableArea.size()); + + double total = 0.0; + const double usablePrimary = getPrimary(usableArea.size()); + + for (const auto& strip : m_strips) { + total += usablePrimary * strip.size; + } + + return total; +} + +double CScrollTapeController::calculateStripStart(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const { + if (stripIndex >= m_strips.size()) + return 0.0; + + const double usablePrimary = getPrimary(usableArea.size()); + double current = 0.0; + + for (size_t i = 0; i < stripIndex; ++i) { + const double stripSize = (fullscreenOnOne && m_strips.size() == 1) ? usablePrimary : usablePrimary * m_strips[i].size; + current += stripSize; + } + + return current; +} + +double CScrollTapeController::calculateStripSize(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const { + if (stripIndex >= m_strips.size()) + return 0.0; + + const double usablePrimary = getPrimary(usableArea.size()); + + if (fullscreenOnOne && m_strips.size() == 1) + return usablePrimary; + + return usablePrimary * m_strips[stripIndex].size; +} + +CBox CScrollTapeController::calculateTargetBox(size_t stripIndex, size_t targetIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne) { + if (stripIndex >= m_strips.size()) + return {}; + + const auto& strip = m_strips[stripIndex]; + if (targetIndex >= strip.targetSizes.size()) + return {}; + + const double usableSecondary = getSecondary(usableArea.size()); + const double usablePrimary = getPrimary(usableArea.size()); + const double cameraOffset = calculateCameraOffset(usableArea, fullscreenOnOne); + + // calculate position along primary axis (strip position) + double primaryPos = calculateStripStart(stripIndex, usableArea, fullscreenOnOne); + double primarySize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne); + + // calculate position along secondary axis (within strip) + double secondaryPos = 0.0; + for (size_t i = 0; i < targetIndex; ++i) { + secondaryPos += strip.targetSizes[i] * usableSecondary; + } + double secondarySize = strip.targetSizes[targetIndex] * usableSecondary; + + // apply camera offset based on direction + // for RIGHT/DOWN: scroll offset moves content left/up (subtract) + // for LEFT/UP: scroll offset moves content right/down (different coordinate system) + if (m_direction == SCROLL_DIR_LEFT) { + // LEFT: flip the entire primary axis, then apply offset + primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset; + } else if (m_direction == SCROLL_DIR_UP) { + // UP: flip the entire primary axis, then apply offset + primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset; + } else { + // RIGHT/DOWN: normal offset + primaryPos -= cameraOffset; + } + + // create the box in primary/secondary coordinates + Vector2D pos = makeVector(primaryPos, secondaryPos); + Vector2D size = makeVector(primarySize, secondarySize); + + // translate to workspace position + pos = pos + workspaceOffset; + + return CBox{pos, size}; +} + +double CScrollTapeController::calculateCameraOffset(const CBox& usableArea, bool fullscreenOnOne) { + const double maxExtent = calculateMaxExtent(usableArea, fullscreenOnOne); + const double usablePrimary = getPrimary(usableArea.size()); + + // don't adjust the offset if we are dragging + if (isBeingDragged()) + return m_offset; + + // if the content fits in viewport, center it + if (maxExtent < usablePrimary) + m_offset = std::round((maxExtent - usablePrimary) / 2.0); + + // if the offset is negative but we already extended, reset offset to 0 + if (maxExtent > usablePrimary && m_offset < 0.0) + m_offset = 0.0; + + return m_offset; +} + +Vector2D CScrollTapeController::getCameraTranslation(const CBox& usableArea, bool fullscreenOnOne) { + const double offset = calculateCameraOffset(usableArea, fullscreenOnOne); + + if (isReversed()) + return makeVector(offset, 0.0); + else + return makeVector(-offset, 0.0); +} + +void CScrollTapeController::centerStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) { + if (stripIndex >= m_strips.size()) + return; + + const double usablePrimary = getPrimary(usableArea.size()); + const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne); + const double stripSize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne); + + m_offset = stripStart - (usablePrimary - stripSize) / 2.0; +} + +void CScrollTapeController::fitStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) { + if (stripIndex >= m_strips.size()) + return; + + const double usablePrimary = getPrimary(usableArea.size()); + const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne); + const double stripSize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne); + + m_offset = std::clamp(m_offset, stripStart - usablePrimary + stripSize, stripStart); +} + +bool CScrollTapeController::isStripVisible(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const { + if (stripIndex >= m_strips.size()) + return false; + + const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne); + const double stripEnd = stripStart + calculateStripSize(stripIndex, usableArea, fullscreenOnOne); + const double viewStart = m_offset; + const double viewEnd = m_offset + getPrimary(usableArea.size()); + + return stripStart < viewEnd && viewStart < stripEnd; +} + +size_t CScrollTapeController::getStripAtCenter(const CBox& usableArea, bool fullscreenOnOne) const { + if (m_strips.empty()) + return 0; + + const double usablePrimary = getPrimary(usableArea.size()); + double currentPos = m_offset; + + for (size_t i = 0; i < m_strips.size(); ++i) { + const double stripSize = calculateStripSize(i, usableArea, fullscreenOnOne); + currentPos += stripSize; + + if (currentPos >= usablePrimary / 2.0 - 2.0) + return i; + } + + return m_strips.empty() ? 0 : m_strips.size() - 1; +} + +void CScrollTapeController::swapStrips(size_t a, size_t b) { + if (a >= m_strips.size() || b >= m_strips.size()) + return; + + std::swap(m_strips.at(a), m_strips.at(b)); +} + +bool CScrollTapeController::isBeingDragged() const { + for (const auto& s : m_strips) { + if (!s.userData) + continue; + + for (const auto& d : s.userData->targetDatas) { + if (d->target == g_layoutManager->dragController()->target()) + return true; + } + } + + return false; +} diff --git a/src/layout/algorithm/tiled/scrolling/ScrollTapeController.hpp b/src/layout/algorithm/tiled/scrolling/ScrollTapeController.hpp new file mode 100644 index 00000000..d03a9b94 --- /dev/null +++ b/src/layout/algorithm/tiled/scrolling/ScrollTapeController.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "../../../../helpers/math/Math.hpp" +#include "../../../../helpers/memory/Memory.hpp" +#include + +namespace Layout::Tiled { + + struct SColumnData; + + enum eScrollDirection : uint8_t { + SCROLL_DIR_RIGHT = 0, + SCROLL_DIR_LEFT, + SCROLL_DIR_DOWN, + SCROLL_DIR_UP, + }; + + struct SStripData { + float size = 1.F; // size along primary axis + std::vector targetSizes; // sizes along secondary axis for each target in this strip + WP userData; + + SStripData() = default; + }; + + struct STapeLayoutResult { + CBox box; + size_t stripIndex = 0; + size_t targetIndex = 0; + }; + + class CScrollTapeController { + public: + CScrollTapeController(eScrollDirection direction = SCROLL_DIR_RIGHT); + ~CScrollTapeController() = default; + + void setDirection(eScrollDirection dir); + eScrollDirection getDirection() const; + bool isPrimaryHorizontal() const; + bool isReversed() const; + + size_t addStrip(float size = 1.0F); + void insertStrip(size_t afterIndex, float size = 1.0F); + void removeStrip(size_t index); + size_t stripCount() const; + SStripData& getStrip(size_t index); + const SStripData& getStrip(size_t index) const; + void swapStrips(size_t a, size_t b); + + void setOffset(double offset); + double getOffset() const; + void adjustOffset(double delta); + + double calculateMaxExtent(const CBox& usableArea, bool fullscreenOnOne = false) const; + double calculateStripStart(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const; + double calculateStripSize(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const; + + CBox calculateTargetBox(size_t stripIndex, size_t targetIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne = false); + + double calculateCameraOffset(const CBox& usableArea, bool fullscreenOnOne = false); + Vector2D getCameraTranslation(const CBox& usableArea, bool fullscreenOnOne = false); + + void centerStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false); + void fitStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false); + + bool isStripVisible(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const; + + size_t getStripAtCenter(const CBox& usableArea, bool fullscreenOnOne = false) const; + + private: + eScrollDirection m_direction = SCROLL_DIR_RIGHT; + std::vector m_strips; + double m_offset = 0.0; + + double getPrimary(const Vector2D& v) const; + double getSecondary(const Vector2D& v) const; + void setPrimary(Vector2D& v, double val) const; + void setSecondary(Vector2D& v, double val) const; + bool isBeingDragged() const; + + Vector2D makeVector(double primary, double secondary) const; + }; +}; diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp new file mode 100644 index 00000000..74de48e4 --- /dev/null +++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp @@ -0,0 +1,1412 @@ +#include "ScrollingAlgorithm.hpp" +#include "ScrollTapeController.hpp" + +#include "../../Algorithm.hpp" +#include "../../../space/Space.hpp" +#include "../../../LayoutManager.hpp" + +#include "../../../../Compositor.hpp" +#include "../../../../desktop/state/FocusState.hpp" +#include "../../../../config/ConfigValue.hpp" +#include "../../../../config/ConfigManager.hpp" +#include "../../../../render/Renderer.hpp" +#include "../../../../managers/input/InputManager.hpp" + +#include +#include +#include + +using namespace Hyprutils::String; +using namespace Hyprutils::Utils; +using namespace Layout; +using namespace Layout::Tiled; + +constexpr float MIN_COLUMN_WIDTH = 0.05F; +constexpr float MAX_COLUMN_WIDTH = 1.F; +constexpr float MIN_ROW_HEIGHT = 0.1F; +constexpr float MAX_ROW_HEIGHT = 1.F; + +// +float SColumnData::getColumnWidth() const { + if (!scrollingData || !scrollingData->controller) + return 1.F; + + auto sd = scrollingData.lock(); + if (!sd) + return 1.F; + + int64_t idx = sd->idx(self.lock()); + if (idx < 0 || (size_t)idx >= sd->controller->stripCount()) + return 1.F; + + return sd->controller->getStrip(idx).size; +} + +void SColumnData::setColumnWidth(float width) { + if (!scrollingData || !scrollingData->controller) + return; + + auto sd = scrollingData.lock(); + if (!sd) + return; + + int64_t idx = sd->idx(self.lock()); + if (idx < 0 || (size_t)idx >= sd->controller->stripCount()) + return; + + sd->controller->getStrip(idx).size = width; +} + +float SColumnData::getTargetSize(size_t idx) const { + if (!scrollingData || !scrollingData->controller) + return 1.F; + + auto sd = scrollingData.lock(); + if (!sd) + return 1.F; + + int64_t colIdx = sd->idx(self.lock()); + if (colIdx < 0 || (size_t)colIdx >= sd->controller->stripCount()) + return 1.F; + + const auto& strip = sd->controller->getStrip(colIdx); + if (idx >= strip.targetSizes.size()) + return 1.F; + + return strip.targetSizes[idx]; +} + +void SColumnData::setTargetSize(size_t idx, float size) { + if (!scrollingData || !scrollingData->controller) + return; + + auto sd = scrollingData.lock(); + if (!sd) + return; + + int64_t colIdx = sd->idx(self.lock()); + if (colIdx < 0 || (size_t)colIdx >= sd->controller->stripCount()) + return; + + auto& strip = sd->controller->getStrip(colIdx); + if (idx >= strip.targetSizes.size()) + strip.targetSizes.resize(idx + 1, 1.F); + + strip.targetSizes[idx] = size; +} + +float SColumnData::getTargetSize(SP target) const { + for (size_t i = 0; i < targetDatas.size(); ++i) { + if (targetDatas[i] == target) + return getTargetSize(i); + } + return 1.F; +} + +void SColumnData::setTargetSize(SP target, float size) { + for (size_t i = 0; i < targetDatas.size(); ++i) { + if (targetDatas[i] == target) { + setTargetSize(i, size); + return; + } + } +} + +void SColumnData::add(SP t) { + const float newSize = 1.F / (float)(targetDatas.size() + 1); + + for (size_t i = 0; i < targetDatas.size(); ++i) { + setTargetSize(i, getTargetSize(i) * (float)targetDatas.size() / (float)(targetDatas.size() + 1)); + } + + targetDatas.emplace_back(makeShared(t, self.lock())); + setTargetSize(targetDatas.size() - 1, newSize); +} + +void SColumnData::add(SP t, int after) { + const float newSize = 1.F / (float)(targetDatas.size() + 1); + + for (size_t i = 0; i < targetDatas.size(); ++i) { + setTargetSize(i, getTargetSize(i) * (float)targetDatas.size() / (float)(targetDatas.size() + 1)); + } + + targetDatas.insert(targetDatas.begin() + after + 1, makeShared(t, self.lock())); + + // Sync sizes - need to insert at the right position + if (scrollingData) { + auto sd = scrollingData.lock(); + if (sd && sd->controller) { + int64_t colIdx = sd->idx(self.lock()); + if (colIdx >= 0 && (size_t)colIdx < sd->controller->stripCount()) { + auto& strip = sd->controller->getStrip(colIdx); + strip.targetSizes.insert(strip.targetSizes.begin() + after + 1, newSize); + } + } + } +} + +void SColumnData::add(SP w) { + const float newSize = 1.F / (float)(targetDatas.size() + 1); + + for (size_t i = 0; i < targetDatas.size(); ++i) { + setTargetSize(i, getTargetSize(i) * (float)targetDatas.size() / (float)(targetDatas.size() + 1)); + } + + targetDatas.emplace_back(w); + w->column = self; + setTargetSize(targetDatas.size() - 1, newSize); +} + +void SColumnData::add(SP w, int after) { + const float newSize = 1.F / (float)(targetDatas.size() + 1); + + for (size_t i = 0; i < targetDatas.size(); ++i) { + setTargetSize(i, getTargetSize(i) * (float)targetDatas.size() / (float)(targetDatas.size() + 1)); + } + + targetDatas.insert(targetDatas.begin() + after + 1, w); + w->column = self; + + // Sync sizes + if (scrollingData) { + auto sd = scrollingData.lock(); + if (sd && sd->controller) { + int64_t colIdx = sd->idx(self.lock()); + if (colIdx >= 0 && (size_t)colIdx < sd->controller->stripCount()) { + auto& strip = sd->controller->getStrip(colIdx); + strip.targetSizes.insert(strip.targetSizes.begin() + after + 1, newSize); + } + } + } +} + +size_t SColumnData::idx(SP t) { + for (size_t i = 0; i < targetDatas.size(); ++i) { + if (targetDatas[i]->target == t) + return i; + } + return 0; +} + +size_t SColumnData::idxForHeight(float y) { + for (size_t i = 0; i < targetDatas.size(); ++i) { + if (targetDatas[i]->target->position().y < y) + continue; + return i - 1; + } + return targetDatas.size() - 1; +} + +void SColumnData::remove(SP t) { + const auto SIZE_BEFORE = targetDatas.size(); + size_t removedIdx = 0; + bool found = false; + + for (size_t i = 0; i < targetDatas.size(); ++i) { + if (targetDatas[i]->target == t) { + removedIdx = i; + found = true; + break; + } + } + + std::erase_if(targetDatas, [&t](const auto& e) { return e->target == t; }); + + if (SIZE_BEFORE == targetDatas.size() && SIZE_BEFORE > 0) + return; + + if (found && scrollingData) { + auto sd = scrollingData.lock(); + if (sd && sd->controller) { + int64_t colIdx = sd->idx(self.lock()); + if (colIdx >= 0 && (size_t)colIdx < sd->controller->stripCount()) { + auto& strip = sd->controller->getStrip(colIdx); + if (removedIdx < strip.targetSizes.size()) { + strip.targetSizes.erase(strip.targetSizes.begin() + removedIdx); + } + } + } + } + + // Renormalize sizes + float newMaxSize = 0.F; + for (size_t i = 0; i < targetDatas.size(); ++i) { + newMaxSize += getTargetSize(i); + } + + if (newMaxSize > 0.F) { + for (size_t i = 0; i < targetDatas.size(); ++i) { + setTargetSize(i, getTargetSize(i) / newMaxSize); + } + } + + if (targetDatas.empty() && scrollingData) + scrollingData->remove(self.lock()); +} + +void SColumnData::up(SP w) { + for (size_t i = 1; i < targetDatas.size(); ++i) { + if (targetDatas[i] != w) + continue; + + std::swap(targetDatas[i], targetDatas[i - 1]); + break; + } +} + +void SColumnData::down(SP w) { + for (size_t i = 0; i < targetDatas.size() - 1; ++i) { + if (targetDatas[i] != w) + continue; + + std::swap(targetDatas[i], targetDatas[i + 1]); + break; + } +} + +SP SColumnData::next(SP w) { + for (size_t i = 0; i < targetDatas.size() - 1; ++i) { + if (targetDatas[i] != w) + continue; + + return targetDatas[i + 1]; + } + + return nullptr; +} + +SP SColumnData::prev(SP w) { + for (size_t i = 1; i < targetDatas.size(); ++i) { + if (targetDatas[i] != w) + continue; + + return targetDatas[i - 1]; + } + + return nullptr; +} + +bool SColumnData::has(SP t) { + return std::ranges::find_if(targetDatas, [t](const auto& e) { return e->target == t; }) != targetDatas.end(); +} + +SScrollingData::SScrollingData(CScrollingAlgorithm* algo) : algorithm(algo) { + controller = makeUnique(SCROLL_DIR_RIGHT); +} + +SP SScrollingData::add() { + static const auto PCOLWIDTH = CConfigValue("scrolling:column_width"); + auto col = columns.emplace_back(makeShared(self.lock())); + col->self = col; + + size_t stripIdx = controller->addStrip(*PCOLWIDTH); + controller->getStrip(stripIdx).userData = col; + + return col; +} + +SP SScrollingData::add(int after) { + static const auto PCOLWIDTH = CConfigValue("scrolling:column_width"); + auto col = makeShared(self.lock()); + col->self = col; + columns.insert(columns.begin() + after + 1, col); + + controller->insertStrip(after, *PCOLWIDTH); + controller->getStrip(after + 1).userData = col; + + return col; +} + +int64_t SScrollingData::idx(SP c) { + for (size_t i = 0; i < columns.size(); ++i) { + if (columns[i] == c) + return i; + } + + return -1; +} + +void SScrollingData::remove(SP c) { + // find index before removing + int64_t index = idx(c); + + std::erase(columns, c); + + // sync with controller + if (index >= 0) + controller->removeStrip(index); +} + +SP SScrollingData::next(SP c) { + for (size_t i = 0; i < columns.size(); ++i) { + if (columns[i] != c) + continue; + + if (i == columns.size() - 1) + return nullptr; + + return columns[i + 1]; + } + + return nullptr; +} + +SP SScrollingData::prev(SP c) { + for (size_t i = 0; i < columns.size(); ++i) { + if (columns[i] != c) + continue; + + if (i == 0) + return nullptr; + + return columns[i - 1]; + } + + return nullptr; +} + +void SScrollingData::centerCol(SP c) { + if (!c) + return; + + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + const auto USABLE = algorithm->usableArea(); + int64_t colIdx = idx(c); + + if (colIdx >= 0) + controller->centerStrip(colIdx, USABLE, *PFSONONE); +} + +void SScrollingData::fitCol(SP c) { + if (!c) + return; + + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + const auto USABLE = algorithm->usableArea(); + int64_t colIdx = idx(c); + + if (colIdx >= 0) + controller->fitStrip(colIdx, USABLE, *PFSONONE); +} + +void SScrollingData::centerOrFitCol(SP c) { + if (!c) + return; + + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); + + if (*PFITMETHOD == 1) + fitCol(c); + else + centerCol(c); +} + +SP SScrollingData::atCenter() { + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + const auto USABLE = algorithm->usableArea(); + + size_t centerIdx = controller->getStripAtCenter(USABLE, *PFSONONE); + + if (centerIdx < columns.size()) + return columns[centerIdx]; + + return nullptr; +} + +void SScrollingData::recalculate(bool forceInstant) { + if (algorithm->m_parent->space()->workspace()->m_hasFullscreenWindow) + return; + + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + + const CBox USABLE = algorithm->usableArea(); + const auto WORKAREA = algorithm->m_parent->space()->workArea(); + + controller->setDirection(algorithm->getDynamicDirection()); + + for (size_t i = 0; i < columns.size(); ++i) { + const auto& COL = columns[i]; + + for (size_t j = 0; j < COL->targetDatas.size(); ++j) { + const auto& TARGET = COL->targetDatas[j]; + + TARGET->layoutBox = controller->calculateTargetBox(i, j, USABLE, WORKAREA.pos(), *PFSONONE); + + if (TARGET->target) + TARGET->target->setPositionGlobal(TARGET->layoutBox); + if (forceInstant && TARGET->target) + TARGET->target->warpPositionSize(); + } + } +} + +double SScrollingData::maxWidth() { + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + const auto USABLE = algorithm->usableArea(); + + return controller->calculateMaxExtent(USABLE, *PFSONONE); +} + +bool SScrollingData::visible(SP c) { + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + const auto USABLE = algorithm->usableArea(); + int64_t colIdx = idx(c); + + if (colIdx >= 0) + return controller->isStripVisible(colIdx, USABLE, *PFSONONE); + + return false; +} + +CScrollingAlgorithm::CScrollingAlgorithm() { + static const auto PCONFWIDTHS = CConfigValue("scrolling:explicit_column_widths"); + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + + m_scrollingData = makeShared(this); + m_scrollingData->self = m_scrollingData; + + // Helper to parse direction string + auto parseDirection = [](const std::string& dir) -> eScrollDirection { + if (dir == "left") + return SCROLL_DIR_LEFT; + else if (dir == "down") + return SCROLL_DIR_DOWN; + else if (dir == "up") + return SCROLL_DIR_UP; + else + return SCROLL_DIR_RIGHT; // default + }; + + m_configCallback = g_pHookSystem->hookDynamic("configReloaded", [this, parseDirection](void* hk, SCallbackInfo& info, std::any param) { + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + + m_config.configuredWidths.clear(); + + CConstVarList widths(*PCONFWIDTHS, 0, ','); + for (auto& w : widths) { + try { + m_config.configuredWidths.emplace_back(std::stof(std::string{w})); + } catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); } + } + if (m_config.configuredWidths.empty()) + m_config.configuredWidths = {0.333, 0.5, 0.667, 1.0}; + + // Update scroll direction + m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION)); + }); + + m_mouseButtonCallback = g_pHookSystem->hookDynamic("mouseButton", [this](void* self, SCallbackInfo& info, std::any e) { + auto E = std::any_cast(e); + if (E.state == WL_POINTER_BUTTON_STATE_RELEASED && Desktop::focusState()->window()) + focusOnInput(Desktop::focusState()->window()->layoutTarget(), true); + }); + + m_focusCallback = g_pHookSystem->hookDynamic("activeWindow", [this](void* hk, SCallbackInfo& info, std::any param) { + const auto E = std::any_cast(param); + const auto PWINDOW = E.window; + + if (!PWINDOW) + return; + + static const auto PFOLLOW_FOCUS = CConfigValue("scrolling:follow_focus"); + + if (!*PFOLLOW_FOCUS && !Desktop::isHardInputFocusReason(E.reason)) + return; + + if (PWINDOW->m_workspace != m_parent->space()->workspace()) + return; + + const auto TARGET = PWINDOW->layoutTarget(); + if (!TARGET || TARGET->floating()) + return; + + focusOnInput(TARGET, Desktop::isHardInputFocusReason(E.reason)); + }); + + // Initialize default widths and direction + m_config.configuredWidths = {0.333, 0.5, 0.667, 1.0}; + m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION)); +} + +CScrollingAlgorithm::~CScrollingAlgorithm() { + m_configCallback.reset(); + m_focusCallback.reset(); +} + +void CScrollingAlgorithm::focusOnInput(SP target, bool hardInput) { + static const auto PFOLLOW_FOCUS_MIN_PERC = CConfigValue("scrolling:follow_min_visible"); + + if (!target || target->space() != m_parent->space()) + return; + + const auto TARGETDATA = dataFor(target); + if (!TARGETDATA) + return; + + if (*PFOLLOW_FOCUS_MIN_PERC > 0.F && !hardInput) { + // check how much of the window is visible, unless hard input focus + + const auto IS_HORIZ = m_scrollingData->controller->isPrimaryHorizontal(); + + const auto MON_BOX = m_parent->space()->workspace()->m_monitor->logicalBox(); + const auto TARGET_POS = target->position(); + const double VISIBLE_LEN = IS_HORIZ ? // + std::abs(std::min(MON_BOX.x + MON_BOX.w, TARGET_POS.x + TARGET_POS.w) - (std::max(MON_BOX.x, TARGET_POS.x))) // + : + std::abs(std::min(MON_BOX.y + MON_BOX.h, TARGET_POS.y + TARGET_POS.h) - (std::max(MON_BOX.y, TARGET_POS.y))); + + // if the amount of visible X is below minimum, reject + if (VISIBLE_LEN < (IS_HORIZ ? MON_BOX.w : MON_BOX.h) * std::clamp(*PFOLLOW_FOCUS_MIN_PERC, 0.F, 1.F)) + return; + } + + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); + if (*PFITMETHOD == 1) + m_scrollingData->fitCol(TARGETDATA->column.lock()); + else + m_scrollingData->centerCol(TARGETDATA->column.lock()); + m_scrollingData->recalculate(); +} + +void CScrollingAlgorithm::newTarget(SP target) { + auto droppingOn = Desktop::focusState()->window(); + + if (droppingOn && droppingOn->layoutTarget() == target) + droppingOn = g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); + + SP droppingData = droppingOn ? dataFor(droppingOn->layoutTarget()) : nullptr; + SP droppingColumn = droppingData ? droppingData->column.lock() : nullptr; + + if (!droppingColumn) { + auto col = m_scrollingData->add(); + col->add(target); + m_scrollingData->fitCol(col); + } else { + if (g_layoutManager->dragController()->wasDraggingWindow() && g_layoutManager->dragController()->draggingTiled()) { + if (droppingOn) { + const auto IDX = droppingColumn->idx(droppingOn->layoutTarget()); + const auto TOP = droppingOn->getWindowIdealBoundingBoxIgnoreReserved().middle().y > g_pInputManager->getMouseCoordsInternal().y; + droppingColumn->add(target, TOP ? (IDX == 0 ? -1 : IDX - 1) : (IDX)); + } else + droppingColumn->add(target); + m_scrollingData->fitCol(droppingColumn); + } else { + auto idx = m_scrollingData->idx(droppingColumn); + auto col = idx == -1 ? m_scrollingData->add() : m_scrollingData->add(idx); + col->add(target); + m_scrollingData->fitCol(col); + } + } + + m_scrollingData->recalculate(); +} + +void CScrollingAlgorithm::movedTarget(SP target, std::optional focalPoint) { + newTarget(target); +} + +void CScrollingAlgorithm::removeTarget(SP target) { + const auto DATA = dataFor(target); + + if (!DATA) + return; + + if (!m_scrollingData->next(DATA->column.lock()) && DATA->column->targetDatas.size() <= 1) { + // move the view if this is the last column + const auto USABLE = usableArea(); + m_scrollingData->controller->adjustOffset(-(USABLE.w * DATA->column->getColumnWidth())); + } + + DATA->column->remove(target); + + if (!DATA->column) { + // column got removed, let's ensure we don't leave any cringe extra space + const auto USABLE = usableArea(); + double newOffset = std::clamp(m_scrollingData->controller->getOffset(), 0.0, std::max(m_scrollingData->maxWidth() - USABLE.w, 1.0)); + m_scrollingData->controller->setOffset(newOffset); + } + + m_scrollingData->recalculate(); +} + +void CScrollingAlgorithm::resizeTarget(const Vector2D& delta, SP target, eRectCorner corner) { + if (!validMapped(target->window())) + return; + + const auto DATA = dataFor(target); + + if (!DATA) { + const auto PWINDOW = target->window(); + *PWINDOW->m_realSize = (PWINDOW->m_realSize->goal() + delta) + .clamp(PWINDOW->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}), + PWINDOW->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY})); + PWINDOW->updateWindowDecos(); + return; + } + + if (!DATA->column || !DATA->column->scrollingData) + return; + + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + + const auto ADJUSTED_DELTA = m_scrollingData->controller->isPrimaryHorizontal() ? delta : Vector2D{delta.y, delta.x}; + const auto USABLE = usableArea(); + const auto DELTA_AS_PERC = ADJUSTED_DELTA / USABLE.size(); + Vector2D modDelta = ADJUSTED_DELTA; + + const auto CURR_COLUMN = DATA->column.lock(); + const int64_t COL_IDX = m_scrollingData->idx(CURR_COLUMN); + + if (COL_IDX < 0) + return; + + const double currentStart = m_scrollingData->controller->calculateStripStart(COL_IDX, USABLE, *PFSONONE); + const double currentSize = m_scrollingData->controller->calculateStripSize(COL_IDX, USABLE, *PFSONONE); + const double currentEnd = currentStart + currentSize; + + const double cameraOffset = m_scrollingData->controller->getOffset(); + const bool isPrimaryHoriz = m_scrollingData->controller->isPrimaryHorizontal(); + const double usablePrimary = isPrimaryHoriz ? USABLE.w : USABLE.h; + + const double onScreenStart = currentStart - cameraOffset; + const double onScreenEnd = currentEnd - cameraOffset; + + // set the offset because we'll prevent centering during a drag + m_scrollingData->controller->setOffset(cameraOffset); + + const bool RESIZING_LEFT = isPrimaryHoriz ? corner == CORNER_BOTTOMLEFT || corner == CORNER_TOPLEFT : corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT; + + if (RESIZING_LEFT) { + // resize from left edge (inner edge) - grow/shrink column width and adjust offset to keep RIGHT edge stationary + const float oldWidth = CURR_COLUMN->getColumnWidth(); + const float requestedDelta = -(float)DELTA_AS_PERC.x; // negative delta means grow when dragging left + float actualDelta = requestedDelta; + + // clamp delta so we don't shrink below MIN or grow above MAX + const float newWidthUnclamped = oldWidth + actualDelta; + const float newWidthClamped = std::clamp(newWidthUnclamped, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH); + actualDelta = newWidthClamped - oldWidth; + + if (actualDelta * usablePrimary > onScreenStart) + actualDelta = onScreenStart / usablePrimary; + + if (actualDelta != 0.F) { + CURR_COLUMN->setColumnWidth(oldWidth + actualDelta); + // adjust camera offset so the RIGHT edge stays stationary on screen + // when column grows (actualDelta > 0), we need to increase offset by the same amount + m_scrollingData->controller->adjustOffset(actualDelta * usablePrimary); + } + + } else { + // resize from right edge (outer edge) - adjust column width only, keep left edge fixed + const float oldWidth = CURR_COLUMN->getColumnWidth(); + const float requestedDelta = (float)DELTA_AS_PERC.x; + float actualDelta = requestedDelta; + + // clamp delta so we don't shrink below MIN or grow above MAX + const float newWidthUnclamped = oldWidth + actualDelta; + const float newWidthClamped = std::clamp(newWidthUnclamped, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH); + actualDelta = newWidthClamped - oldWidth; + + // also clamp so right edge doesn't go past right viewport boundary + if (onScreenEnd + (actualDelta * usablePrimary) > usablePrimary) + actualDelta = (usablePrimary - onScreenEnd) / usablePrimary; + + if (actualDelta != 0.F) + CURR_COLUMN->setColumnWidth(oldWidth + actualDelta); + } + + if (DATA->column->targetDatas.size() > 1) { + const auto& CURR_TD = DATA; + const auto NEXT_TD = DATA->column->next(DATA); + const auto PREV_TD = DATA->column->prev(DATA); + if (corner == CORNER_NONE) { + if (!PREV_TD) + corner = CORNER_BOTTOMRIGHT; + else { + corner = CORNER_TOPRIGHT; + modDelta.y *= -1.0f; + } + } + + switch (corner) { + case CORNER_BOTTOMLEFT: + case CORNER_BOTTOMRIGHT: { + if (!NEXT_TD) + break; + + float nextSize = CURR_COLUMN->getTargetSize(NEXT_TD); + float currSize = CURR_COLUMN->getTargetSize(CURR_TD); + + if (nextSize <= MIN_ROW_HEIGHT && delta.y >= 0) + break; + + float adjust = std::clamp((float)(delta.y / USABLE.h), (-currSize + MIN_ROW_HEIGHT), (nextSize - MIN_ROW_HEIGHT)); + + CURR_COLUMN->setTargetSize(NEXT_TD, std::clamp(nextSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT)); + CURR_COLUMN->setTargetSize(CURR_TD, std::clamp(currSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT)); + break; + } + case CORNER_TOPLEFT: + case CORNER_TOPRIGHT: { + if (!PREV_TD) + break; + + float prevSize = CURR_COLUMN->getTargetSize(PREV_TD); + float currSize = CURR_COLUMN->getTargetSize(CURR_TD); + + if ((prevSize <= MIN_ROW_HEIGHT && modDelta.y <= 0) || (currSize <= MIN_ROW_HEIGHT && delta.y >= 0)) + break; + + float adjust = std::clamp((float)(modDelta.y / USABLE.h), -(prevSize - MIN_ROW_HEIGHT), (currSize - MIN_ROW_HEIGHT)); + + CURR_COLUMN->setTargetSize(PREV_TD, std::clamp(prevSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT)); + CURR_COLUMN->setTargetSize(CURR_TD, std::clamp(currSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT)); + break; + } + + default: break; + } + } + + m_scrollingData->recalculate(true); +} + +void CScrollingAlgorithm::recalculate() { + if (Desktop::focusState()->window()) + focusOnInput(Desktop::focusState()->window()->layoutTarget(), true); + + m_scrollingData->recalculate(); +} + +SP CScrollingAlgorithm::closestNode(const Vector2D& posGlobglobgabgalab) { + SP res = nullptr; + double distClosest = -1; + for (auto& c : m_scrollingData->columns) { + for (auto& n : c->targetDatas) { + if (n->target && Desktop::View::validMapped(n->target->window())) { + auto distAnother = vecToRectDistanceSquared(posGlobglobgabgalab, n->layoutBox.pos(), n->layoutBox.pos() + n->layoutBox.size()); + if (!res || distAnother < distClosest) { + res = n; + distClosest = distAnother; + } + } + } + } + return res; +} + +SP CScrollingAlgorithm::getNextCandidate(SP old) { + const auto CENTER = old->position().middle(); + + const auto NODE = closestNode(CENTER); + + if (!NODE) + return nullptr; + + return NODE->target.lock(); +} + +void CScrollingAlgorithm::swapTargets(SP a, SP b) { + auto nodeA = dataFor(a); + auto nodeB = dataFor(b); + + if (nodeA) + nodeA->target = b; + if (nodeB) + nodeB->target = a; + + m_scrollingData->recalculate(); +} + +void CScrollingAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + moveTargetTo(t, dir, silent); +} + +void CScrollingAlgorithm::moveTargetTo(SP t, Math::eDirection dir, bool silent) { + const auto DATA = dataFor(t); + + if (!DATA) + return; + + const auto TAPE_DIR = getDynamicDirection(); + const auto CURRENT_COL = DATA->column.lock(); + const auto current_idx = m_scrollingData->idx(CURRENT_COL); + + if (dir == Math::DIRECTION_LEFT) { + const auto COL = m_scrollingData->prev(DATA->column.lock()); + + // ignore moves to the "origin" when on first column and moving opposite to tape direction + if (!COL && current_idx == 0 && (TAPE_DIR == SCROLL_DIR_RIGHT || TAPE_DIR == SCROLL_DIR_DOWN)) + return; + + DATA->column->remove(t); + + if (!COL) { + const auto NEWCOL = m_scrollingData->add(-1); + NEWCOL->add(DATA); + m_scrollingData->centerOrFitCol(NEWCOL); + } else { + if (COL->targetDatas.size() > 0) + COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y)); + else + COL->add(DATA); + m_scrollingData->centerOrFitCol(COL); + } + } else if (dir == Math::DIRECTION_RIGHT) { + const auto COL = m_scrollingData->next(DATA->column.lock()); + + // ignore moves to the "origin" when on last column and moving opposite to tape direction + if (!COL && current_idx == (int64_t)m_scrollingData->columns.size() - 1 && (TAPE_DIR == SCROLL_DIR_LEFT || TAPE_DIR == SCROLL_DIR_UP)) + return; + + DATA->column->remove(t); + + if (!COL) { + // make a new one + const auto NEWCOL = m_scrollingData->add(); + NEWCOL->add(DATA); + m_scrollingData->centerOrFitCol(NEWCOL); + } else { + if (COL->targetDatas.size() > 0) + COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y)); + else + COL->add(DATA); + m_scrollingData->centerOrFitCol(COL); + } + + } else if (dir == Math::DIRECTION_UP) + DATA->column->up(DATA); + else if (dir == Math::DIRECTION_DOWN) + DATA->column->down(DATA); + + m_scrollingData->recalculate(); + focusTargetUpdate(t); + if (t->window()) + g_pCompositor->warpCursorTo(t->window()->middle()); +} + +std::expected CScrollingAlgorithm::layoutMsg(const std::string_view& sv) { + auto centerOrFit = [this](const SP COL) -> void { + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); + if (*PFITMETHOD == 1) + m_scrollingData->fitCol(COL); + else + m_scrollingData->centerCol(COL); + }; + + const auto ARGS = CVarList(std::string{sv}, 0, ' '); + if (ARGS[0] == "move") { + if (ARGS[1] == "+col" || ARGS[1] == "col") { + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + if (!TDATA) + return std::unexpected("no window"); + + const auto COL = m_scrollingData->next(TDATA->column.lock()); + if (!COL) { + // move to max + double maxOffset = m_scrollingData->maxWidth(); + m_scrollingData->controller->setOffset(maxOffset); + m_scrollingData->recalculate(); + focusTargetUpdate(nullptr); + return {}; + } + + centerOrFit(COL); + m_scrollingData->recalculate(); + + focusTargetUpdate(COL->targetDatas.front()->target.lock()); + if (COL->targetDatas.front()->target->window()) + g_pCompositor->warpCursorTo(COL->targetDatas.front()->target->window()->middle()); + + return {}; + } else if (ARGS[1] == "-col") { + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + if (!TDATA) { + if (m_scrollingData->columns.size() > 0) { + m_scrollingData->centerCol(m_scrollingData->columns.back()); + m_scrollingData->recalculate(); + focusTargetUpdate((m_scrollingData->columns.back()->targetDatas.back())->target.lock()); + if (m_scrollingData->columns.back()->targetDatas.back()->target->window()) + g_pCompositor->warpCursorTo((m_scrollingData->columns.back()->targetDatas.back())->target->window()->middle()); + } + + return {}; + } + + const auto COL = m_scrollingData->prev(TDATA->column.lock()); + if (!COL) + return {}; + + centerOrFit(COL); + m_scrollingData->recalculate(); + + focusTargetUpdate(COL->targetDatas.back()->target.lock()); + if (COL->targetDatas.front()->target->window()) + g_pCompositor->warpCursorTo(COL->targetDatas.front()->target->window()->middle()); + + return {}; + } + + const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0); + + if (!PLUSMINUS.has_value()) + return std::unexpected("failed to parse offset"); + + m_scrollingData->controller->adjustOffset(-(*PLUSMINUS)); + m_scrollingData->recalculate(); + + const auto ATCENTER = m_scrollingData->atCenter(); + + focusTargetUpdate(ATCENTER ? (*ATCENTER->targetDatas.begin())->target.lock() : nullptr); + } else if (ARGS[0] == "colresize") { + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + + if (!TDATA) + return {}; + + if (ARGS[1] == "all") { + float abs = 0; + try { + abs = std::stof(ARGS[2]); + } catch (...) { return {}; } + + for (const auto& c : m_scrollingData->columns) { + c->setColumnWidth(abs); + } + + m_scrollingData->recalculate(); + return {}; + } + + CScopeGuard x([this, TDATA] { + auto col = TDATA->column.lock(); + if (col) { + col->setColumnWidth(std::clamp(col->getColumnWidth(), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH)); + m_scrollingData->centerOrFitCol(col); + } + m_scrollingData->recalculate(); + }); + + if (ARGS[1][0] == '+' || ARGS[1][0] == '-') { + if (ARGS[1] == "+conf") { + auto col = TDATA->column.lock(); + if (col) { + for (size_t i = 0; i < m_config.configuredWidths.size(); ++i) { + if (m_config.configuredWidths[i] > col->getColumnWidth()) { + col->setColumnWidth(m_config.configuredWidths[i]); + break; + } + + if (i == m_config.configuredWidths.size() - 1) + col->setColumnWidth(m_config.configuredWidths[0]); + } + } + + return {}; + } else if (ARGS[1] == "-conf") { + auto col = TDATA->column.lock(); + if (col) { + for (size_t i = m_config.configuredWidths.size() - 1;; --i) { + if (m_config.configuredWidths[i] < col->getColumnWidth()) { + col->setColumnWidth(m_config.configuredWidths[i]); + break; + } + + if (i == 0) { + col->setColumnWidth(m_config.configuredWidths.back()); + break; + } + } + } + + return {}; + } + + const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0); + + if (!PLUSMINUS.has_value()) + return {}; + + auto col = TDATA->column.lock(); + if (col) + col->setColumnWidth(col->getColumnWidth() + *PLUSMINUS); + } else { + float abs = 0; + try { + abs = std::stof(ARGS[1]); + } catch (...) { return {}; } + + auto col = TDATA->column.lock(); + if (col) + col->setColumnWidth(abs); + } + } else if (ARGS[0] == "fit") { + const auto PWINDOW = Desktop::focusState()->window(); + + if (!PWINDOW) + return std::unexpected("no focused window"); + + const auto WDATA = dataFor(PWINDOW->layoutTarget()); + + if (!WDATA || m_scrollingData->columns.size() == 0) + return std::unexpected("can't fit: no window or columns"); + + if (ARGS[1] == "active") { + // fit the current column to 1.F + const auto USABLE = usableArea(); + + WDATA->column->setColumnWidth(1.F); + + double off = 0.F; + for (size_t i = 0; i < m_scrollingData->columns.size(); ++i) { + if (m_scrollingData->columns[i]->has(PWINDOW->layoutTarget())) + break; + + off += USABLE.w * m_scrollingData->columns[i]->getColumnWidth(); + } + + m_scrollingData->controller->setOffset(off); + m_scrollingData->recalculate(); + } else if (ARGS[1] == "all") { + // fit all columns on screen + const size_t LEN = m_scrollingData->columns.size(); + for (const auto& c : m_scrollingData->columns) { + c->setColumnWidth(1.F / (float)LEN); + } + + m_scrollingData->controller->setOffset(0); + m_scrollingData->recalculate(); + } else if (ARGS[1] == "toend") { + // fit all columns on screen that start from the current and end on the last + bool begun = false; + size_t foundAt = 0; + for (size_t i = 0; i < m_scrollingData->columns.size(); ++i) { + if (!begun && !m_scrollingData->columns[i]->has(PWINDOW->layoutTarget())) + continue; + + if (!begun) { + begun = true; + foundAt = i; + } + + m_scrollingData->columns[i]->setColumnWidth(1.F / (float)(m_scrollingData->columns.size() - foundAt)); + } + + if (!begun) + return std::unexpected("couldn't find beginning"); + + const auto USABLE = usableArea(); + + double off = 0; + for (size_t i = 0; i < foundAt; ++i) { + off += USABLE.w * m_scrollingData->columns[i]->getColumnWidth(); + } + + m_scrollingData->controller->setOffset(off); + m_scrollingData->recalculate(); + } else if (ARGS[1] == "tobeg") { + // fit all columns on screen that start from the current and end on the last + bool begun = false; + size_t foundAt = 0; + for (int64_t i = (int64_t)m_scrollingData->columns.size() - 1; i >= 0; --i) { + if (!begun && !m_scrollingData->columns[i]->has(PWINDOW->layoutTarget())) + continue; + + if (!begun) { + begun = true; + foundAt = i; + } + + m_scrollingData->columns[i]->setColumnWidth(1.F / (float)(foundAt + 1)); + } + + if (!begun) + return {}; + + m_scrollingData->controller->setOffset(0); + m_scrollingData->recalculate(); + } else if (ARGS[1] == "visible") { + // fit all columns on screen that start from the current and end on the last + + bool begun = false; + size_t foundAt = 0; + std::vector> visible; + for (size_t i = 0; i < m_scrollingData->columns.size(); ++i) { + if (!begun && !m_scrollingData->visible(m_scrollingData->columns[i])) + continue; + + if (!begun) { + begun = true; + foundAt = i; + } + + if (!m_scrollingData->visible(m_scrollingData->columns[i])) + break; + + visible.emplace_back(m_scrollingData->columns[i]); + } + + if (!begun) + return {}; + + double off = 0; + + if (foundAt != 0) { + const auto USABLE = usableArea(); + + for (size_t i = 0; i < foundAt; ++i) { + off += USABLE.w * m_scrollingData->columns[i]->getColumnWidth(); + } + } + + for (const auto& v : visible) { + v->setColumnWidth(1.F / (float)visible.size()); + } + + m_scrollingData->controller->setOffset(off); + m_scrollingData->recalculate(); + } + } else if (ARGS[0] == "focus") { + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + static const auto PNOFALLBACK = CConfigValue("general:no_focus_fallback"); + + if (!TDATA || ARGS[1].empty()) + return std::unexpected("no window to focus"); + + // Determine if we're in vertical scroll mode (strips are horizontal) + const bool isVerticalScroll = (getDynamicDirection() == SCROLL_DIR_DOWN || getDynamicDirection() == SCROLL_DIR_UP); + + // Map direction keys based on scroll mode: + // Horizontal scroll (RIGHT/LEFT): u/d move within strip, l/r move between strips + // Vertical scroll (DOWN/UP): l/r move within strip, u/d move between strips + char dirChar = ARGS[1][0]; + + // Convert to semantic directions + bool isPrevInStrip = (!isVerticalScroll && (dirChar == 'u' || dirChar == 't')) || (isVerticalScroll && dirChar == 'l'); + bool isNextInStrip = (!isVerticalScroll && (dirChar == 'b' || dirChar == 'd')) || (isVerticalScroll && dirChar == 'r'); + bool isPrevStrip = (!isVerticalScroll && dirChar == 'l') || (isVerticalScroll && (dirChar == 'u' || dirChar == 't')); + bool isNextStrip = (!isVerticalScroll && dirChar == 'r') || (isVerticalScroll && (dirChar == 'b' || dirChar == 'd')); + + if (isPrevInStrip) { + // Move to previous target within current strip + auto PREV = TDATA->column->prev(TDATA); + if (!PREV) { + if (!*PNOFALLBACK) + PREV = TDATA->column->targetDatas.back(); + else + return std::unexpected("fallback disabled (no target)"); + } + + focusTargetUpdate(PREV->target.lock()); + if (PREV->target->window()) + g_pCompositor->warpCursorTo(PREV->target->window()->middle()); + } else if (isNextInStrip) { + // Move to next target within current strip + auto NEXT = TDATA->column->next(TDATA); + if (!NEXT) { + if (!*PNOFALLBACK) + NEXT = TDATA->column->targetDatas.front(); + else + return std::unexpected("fallback disabled (no target)"); + } + + focusTargetUpdate(NEXT->target.lock()); + if (NEXT->target->window()) + g_pCompositor->warpCursorTo(NEXT->target->window()->middle()); + } else if (isPrevStrip) { + // Move to previous strip + auto PREV = m_scrollingData->prev(TDATA->column.lock()); + if (!PREV) { + if (*PNOFALLBACK) { + centerOrFit(TDATA->column.lock()); + m_scrollingData->recalculate(); + if (TDATA->target->window()) + g_pCompositor->warpCursorTo(TDATA->target->window()->middle()); + return {}; + } else + PREV = m_scrollingData->columns.back(); + } + + auto pTargetData = findBestNeighbor(TDATA, PREV); + if (pTargetData) { + focusTargetUpdate(pTargetData->target.lock()); + centerOrFit(PREV); + m_scrollingData->recalculate(); + if (pTargetData->target->window()) + g_pCompositor->warpCursorTo(pTargetData->target->window()->middle()); + } + } else if (isNextStrip) { + // Move to next strip + auto NEXT = m_scrollingData->next(TDATA->column.lock()); + if (!NEXT) { + if (*PNOFALLBACK) { + centerOrFit(TDATA->column.lock()); + m_scrollingData->recalculate(); + if (TDATA->target->window()) + g_pCompositor->warpCursorTo(TDATA->target->window()->middle()); + return {}; + } else + NEXT = m_scrollingData->columns.front(); + } + + auto pTargetData = findBestNeighbor(TDATA, NEXT); + if (pTargetData) { + focusTargetUpdate(pTargetData->target.lock()); + centerOrFit(NEXT); + m_scrollingData->recalculate(); + if (pTargetData->target->window()) + g_pCompositor->warpCursorTo(pTargetData->target->window()->middle()); + } + } + } else if (ARGS[0] == "promote") { + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + + if (!TDATA) + return std::unexpected("no window focused"); + + auto idx = m_scrollingData->idx(TDATA->column.lock()); + auto col = idx == -1 ? m_scrollingData->add() : m_scrollingData->add(idx); + + TDATA->column->remove(TDATA->target.lock()); + + col->add(TDATA); + + m_scrollingData->recalculate(); + } else if (ARGS[0] == "swapcol") { + if (ARGS.size() < 2) + return std::unexpected("not enough args"); + + const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); + if (!TDATA) + return std::unexpected("no window"); + + const auto CURRENT_COL = TDATA->column.lock(); + if (!CURRENT_COL) + return std::unexpected("no current col"); + + if (m_scrollingData->columns.size() < 2) + return std::unexpected("not enough columns to swap"); + + const int64_t currentIdx = m_scrollingData->idx(CURRENT_COL); + const size_t colCount = m_scrollingData->columns.size(); + + if (currentIdx == -1) + return std::unexpected("no current column"); + + const std::string& direction = ARGS[1]; + int64_t targetIdx = -1; + + // wrap around swaps + if (direction == "l") + targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1); + else if (direction == "r") + targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1); + else + return std::unexpected("no target (invalid direction?)"); + ; + + std::swap(m_scrollingData->columns.at(currentIdx), m_scrollingData->columns.at(targetIdx)); + + m_scrollingData->controller->swapStrips(currentIdx, targetIdx); + + m_scrollingData->centerOrFitCol(CURRENT_COL); + m_scrollingData->recalculate(); + } else + return std::unexpected("no such layoutmsg for scrolling"); + + return {}; +} + +std::optional CScrollingAlgorithm::predictSizeForNewTarget() { + return std::nullopt; +} + +void CScrollingAlgorithm::focusTargetUpdate(SP target) { + if (!target || !validMapped(target->window())) { + Desktop::focusState()->fullWindowFocus(nullptr, Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); + return; + } + Desktop::focusState()->fullWindowFocus(target->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); + const auto TARGETDATA = dataFor(target); + if (TARGETDATA) { + if (auto col = TARGETDATA->column.lock()) + col->lastFocusedTarget = TARGETDATA; + } +} + +SP CScrollingAlgorithm::findBestNeighbor(SP pCurrent, SP pTargetCol) { + if (!pCurrent || !pTargetCol || pTargetCol->targetDatas.empty()) + return nullptr; + + const double currentTop = pCurrent->layoutBox.y; + const double currentBottom = pCurrent->layoutBox.y + pCurrent->layoutBox.h; + std::vector> overlappingTargets; + for (const auto& candidate : pTargetCol->targetDatas) { + const double candidateTop = candidate->layoutBox.y; + const double candidateBottom = candidate->layoutBox.y + candidate->layoutBox.h; + const bool overlaps = (candidateTop < currentBottom) && (candidateBottom > currentTop); + + if (overlaps) + overlappingTargets.emplace_back(candidate); + } + if (!overlappingTargets.empty()) { + auto lastFocused = pTargetCol->lastFocusedTarget.lock(); + + if (lastFocused) { + auto it = std::ranges::find(overlappingTargets, lastFocused); + if (it != overlappingTargets.end()) + return lastFocused; + } + + auto topmost = std::ranges::min_element(overlappingTargets, std::less<>{}, [](const SP& t) { return t->layoutBox.y; }); + return *topmost; + } + if (!pTargetCol->targetDatas.empty()) + return pTargetCol->targetDatas.front(); + return nullptr; +} + +SP CScrollingAlgorithm::dataFor(SP t) { + if (!t) + return nullptr; + + for (const auto& c : m_scrollingData->columns) { + for (const auto& d : c->targetDatas) { + if (d->target == t) + return d; + } + } + + return nullptr; +} + +eScrollDirection CScrollingAlgorithm::getDynamicDirection() { + const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(m_parent->space()->workspace()); + std::string directionString; + if (WORKSPACERULE.layoutopts.contains("direction")) + directionString = WORKSPACERULE.layoutopts.at("direction"); + + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + std::string configDirection = *PCONFDIRECTION; + + // Workspace rule overrides global config + if (!directionString.empty()) + configDirection = directionString; + + // Parse direction string + if (configDirection == "left") + return SCROLL_DIR_LEFT; + else if (configDirection == "down") + return SCROLL_DIR_DOWN; + else if (configDirection == "up") + return SCROLL_DIR_UP; + else + return SCROLL_DIR_RIGHT; // default +} + +CBox CScrollingAlgorithm::usableArea() { + CBox box = m_parent->space()->workArea(); + box.translate(-m_parent->space()->workspace()->m_monitor->m_position); + return box; +} diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp new file mode 100644 index 00000000..a2a9316e --- /dev/null +++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include "../../TiledAlgorithm.hpp" +#include "../../../../managers/HookSystemManager.hpp" +#include "../../../../helpers/math/Direction.hpp" +#include "ScrollTapeController.hpp" + +#include + +namespace Layout::Tiled { + class CScrollingAlgorithm; + struct SColumnData; + struct SScrollingData; + + struct SScrollingTargetData { + SScrollingTargetData(SP t, SP col) : target(t), column(col) { + ; + } + + WP target; + WP column; + bool ignoreFullscreenChecks = false; + + CBox layoutBox; + }; + + struct SColumnData { + SColumnData(SP data) : scrollingData(data) { + ; + } + + void add(SP t); + void add(SP t, int after); + void add(SP w); + void add(SP w, int after); + void remove(SP t); + bool has(SP t); + size_t idx(SP t); + + // index of lowest target that is above y. + size_t idxForHeight(float y); + + void up(SP w); + void down(SP w); + + SP next(SP w); + SP prev(SP w); + + std::vector> targetDatas; + WP scrollingData; + WP lastFocusedTarget; + + WP self; + + // Helper methods to access controller-managed data + float getColumnWidth() const; + void setColumnWidth(float width); + float getTargetSize(size_t idx) const; + void setTargetSize(size_t idx, float size); + float getTargetSize(SP target) const; + void setTargetSize(SP target, float size); + }; + + struct SScrollingData { + SScrollingData(CScrollingAlgorithm* algo); + + std::vector> columns; + + UP controller; + + SP add(); + SP add(int after); + int64_t idx(SP c); + void remove(SP c); + double maxWidth(); + SP next(SP c); + SP prev(SP c); + SP atCenter(); + + bool visible(SP c); + void centerCol(SP c); + void fitCol(SP c); + void centerOrFitCol(SP c); + + void recalculate(bool forceInstant = false); + + CScrollingAlgorithm* algorithm = nullptr; + WP self; + std::optional lockedCameraOffset; + }; + + class CScrollingAlgorithm : public ITiledAlgorithm { + public: + CScrollingAlgorithm(); + virtual ~CScrollingAlgorithm(); + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); + + virtual SP getNextCandidate(SP old); + + virtual std::expected layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); + + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + CBox usableArea(); + + private: + SP m_scrollingData; + + SP m_configCallback; + SP m_focusCallback; + SP m_mouseButtonCallback; + + struct { + std::vector configuredWidths; + } m_config; + + eScrollDirection getDynamicDirection(); + + SP findBestNeighbor(SP pCurrent, SP pTargetCol); + SP dataFor(SP t); + SP closestNode(const Vector2D& posGlobglobgabgalab); + + void focusTargetUpdate(SP target); + void moveTargetTo(SP t, Math::eDirection dir, bool silent); + void focusOnInput(SP target, bool hardInput); + + friend struct SScrollingData; + }; +}; diff --git a/src/layout/space/Space.cpp b/src/layout/space/Space.cpp new file mode 100644 index 00000000..742c398a --- /dev/null +++ b/src/layout/space/Space.cpp @@ -0,0 +1,185 @@ +#include "Space.hpp" + +#include "../target/Target.hpp" +#include "../algorithm/Algorithm.hpp" + +#include "../../debug/log/Logger.hpp" +#include "../../desktop/Workspace.hpp" +#include "../../config/ConfigManager.hpp" + +using namespace Layout; + +SP CSpace::create(PHLWORKSPACE w) { + auto space = SP(new CSpace(w)); + space->m_self = space; + return space; +} + +CSpace::CSpace(PHLWORKSPACE parent) : m_parent(parent) { + recheckWorkArea(); +} + +void CSpace::add(SP t) { + m_targets.emplace_back(t); + + recheckWorkArea(); + + if (m_algorithm) + m_algorithm->addTarget(t); + + m_parent->updateWindows(); +} + +void CSpace::move(SP t, std::optional focalPoint) { + m_targets.emplace_back(t); + + recheckWorkArea(); + + if (m_algorithm) + m_algorithm->moveTarget(t, focalPoint); + + m_parent->updateWindows(); +} + +void CSpace::remove(SP t) { + std::erase_if(m_targets, [&t](const auto& e) { return !e || e == t; }); + + recheckWorkArea(); + + if (m_algorithm) + m_algorithm->removeTarget(t); + + if (m_parent) // can be null if the workspace is gone + m_parent->updateWindows(); +} + +void CSpace::setAlgorithmProvider(SP algo) { + m_algorithm = algo; +} + +void CSpace::recheckWorkArea() { + if (!m_parent || !m_parent->m_monitor) { + Log::logger->log(Log::ERR, "CSpace: recheckWorkArea on no parent / mon?!"); + return; + } + + const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(m_parent.lock()); + + auto workArea = m_parent->m_monitor->logicalBoxMinusReserved(); + + static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); + static auto PFLOATGAPSDATA = CConfigValue("general:float_gaps"); + auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())->getData()); + auto* PFLOATGAPS = sc(PFLOATGAPSDATA.ptr()->getData()); + if (PFLOATGAPS->m_bottom < 0 || PFLOATGAPS->m_left < 0 || PFLOATGAPS->m_right < 0 || PFLOATGAPS->m_top < 0) + PFLOATGAPS = PGAPSOUT; + + auto gapsOut = WORKSPACERULE.gapsOut.value_or(*PGAPSOUT); + auto gapsFloat = WORKSPACERULE.gapsOut.value_or(*PFLOATGAPS); + + Desktop::CReservedArea reservedGaps{gapsOut.m_top, gapsOut.m_right, gapsOut.m_bottom, gapsOut.m_left}; + Desktop::CReservedArea reservedFloatGaps{gapsFloat.m_top, gapsFloat.m_right, gapsFloat.m_bottom, gapsFloat.m_left}; + + auto floatWorkArea = workArea; + + reservedFloatGaps.applyip(floatWorkArea); + reservedGaps.applyip(workArea); + + m_workArea = workArea; + m_floatingWorkArea = floatWorkArea; +} + +const CBox& CSpace::workArea(bool floating) const { + return floating ? m_floatingWorkArea : m_workArea; +} + +PHLWORKSPACE CSpace::workspace() const { + return m_parent.lock(); +} + +void CSpace::toggleTargetFloating(SP t) { + t->setWasTiling(true); + m_algorithm->setFloating(t, !t->floating()); + t->setWasTiling(false); + + m_parent->updateWindows(); + + recalculate(); +} + +CBox CSpace::targetPositionLocal(SP t) const { + return t->position().translate(-m_workArea.pos()); +} + +void CSpace::resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner) { + if (!m_algorithm) + return; + + m_algorithm->resizeTarget(Δ, target, corner); +} + +void CSpace::moveTarget(const Vector2D& Δ, SP target) { + if (!m_algorithm) + return; + + m_algorithm->moveTarget(Δ, target); +} + +SP CSpace::algorithm() const { + return m_algorithm; +} + +void CSpace::recalculate() { + recheckWorkArea(); + + if (m_algorithm) + m_algorithm->recalculate(); +} + +void CSpace::setFullscreen(SP t, eFullscreenMode mode) { + t->setFullscreenMode(mode); + + if (mode == FSMODE_NONE && m_algorithm && t->floating()) + m_algorithm->recenter(t); + + recalculate(); +} + +std::expected CSpace::layoutMsg(const std::string_view& sv) { + if (m_algorithm) + return m_algorithm->layoutMsg(sv); + + return {}; +} + +std::optional CSpace::predictSizeForNewTiledTarget() { + if (m_algorithm) + return m_algorithm->predictSizeForNewTiledTarget(); + + return std::nullopt; +} + +void CSpace::swap(SP a, SP b) { + for (auto& t : m_targets) { + if (t == a) + t = b; + else if (t == b) + t = a; + } + + if (m_algorithm) + m_algorithm->swapTargets(a, b); +} + +void CSpace::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + if (m_algorithm) + m_algorithm->moveTargetInDirection(t, dir, silent); +} + +SP CSpace::getNextCandidate(SP old) { + return !m_algorithm ? nullptr : m_algorithm->getNextCandidate(old); +} + +const std::vector>& CSpace::targets() const { + return m_targets; +} diff --git a/src/layout/space/Space.hpp b/src/layout/space/Space.hpp new file mode 100644 index 00000000..4229e99d --- /dev/null +++ b/src/layout/space/Space.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/math/Direction.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include "../../desktop/DesktopTypes.hpp" + +#include "../LayoutManager.hpp" + +#include +#include + +namespace Layout { + class ITarget; + class CAlgorithm; + + class CSpace { + public: + static SP create(PHLWORKSPACE w); + ~CSpace() = default; + + void add(SP t); + void remove(SP t); + void move(SP t, std::optional focalPoint = std::nullopt); + + void swap(SP a, SP b); + + SP getNextCandidate(SP old); + + void setAlgorithmProvider(SP algo); + void recheckWorkArea(); + void setFullscreen(SP t, eFullscreenMode mode); + + void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + void recalculate(); + + void toggleTargetFloating(SP t); + + std::expected layoutMsg(const std::string_view& sv); + std::optional predictSizeForNewTiledTarget(); + + const CBox& workArea(bool floating = false) const; + PHLWORKSPACE workspace() const; + CBox targetPositionLocal(SP t) const; + + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); + + SP algorithm() const; + + const std::vector>& targets() const; + + private: + CSpace(PHLWORKSPACE parent); + + WP m_self; + + std::vector> m_targets; + SP m_algorithm; + PHLWORKSPACEREF m_parent; + + // work area is in global coords + CBox m_workArea, m_floatingWorkArea; + }; +}; \ No newline at end of file diff --git a/src/layout/supplementary/DragController.cpp b/src/layout/supplementary/DragController.cpp new file mode 100644 index 00000000..a28aef07 --- /dev/null +++ b/src/layout/supplementary/DragController.cpp @@ -0,0 +1,396 @@ +#include "DragController.hpp" + +#include "../space/Space.hpp" + +#include "../../Compositor.hpp" +#include "../../managers/cursor/CursorShapeOverrideController.hpp" +#include "../../desktop/state/FocusState.hpp" +#include "../../desktop/view/Group.hpp" +#include "../../render/Renderer.hpp" + +using namespace Layout; +using namespace Layout::Supplementary; + +SP CDragStateController::target() const { + return m_target.lock(); +} + +eMouseBindMode CDragStateController::mode() const { + return m_dragMode; +} + +bool CDragStateController::wasDraggingWindow() const { + return m_wasDraggingWindow; +} + +bool CDragStateController::dragThresholdReached() const { + return m_dragThresholdReached; +} + +void CDragStateController::resetDragThresholdReached() { + m_dragThresholdReached = false; +} + +bool CDragStateController::draggingTiled() const { + return m_draggingTiled; +} + +bool CDragStateController::updateDragWindow() { + const auto DRAGGINGTARGET = m_target.lock(); + const bool WAS_FULLSCREEN = DRAGGINGTARGET->fullscreenMode() != FSMODE_NONE; + + if (m_dragThresholdReached) { + if (WAS_FULLSCREEN) { + Log::logger->log(Log::DEBUG, "Dragging a fullscreen window"); + g_pCompositor->setWindowFullscreenInternal(DRAGGINGTARGET->window(), FSMODE_NONE); + } + + const auto PWORKSPACE = DRAGGINGTARGET->workspace(); + const auto DRAGGINGWINDOW = DRAGGINGTARGET->window(); + + if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || (!DRAGGINGWINDOW->m_createdOverFullscreen && !DRAGGINGWINDOW->m_pinned))) { + Log::logger->log(Log::DEBUG, "Rejecting drag on a fullscreen workspace. (window under fullscreen)"); + CKeybindManager::changeMouseBindMode(MBIND_INVALID); + return true; + } + } + + m_draggingTiled = false; + m_draggingWindowOriginalFloatSize = DRAGGINGTARGET->lastFloatingSize(); + + if (WAS_FULLSCREEN && DRAGGINGTARGET->floating()) { + const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); + DRAGGINGTARGET->setPositionGlobal(CBox{MOUSECOORDS - DRAGGINGTARGET->position().size() / 2.F, DRAGGINGTARGET->position().size()}); + } else if (!DRAGGINGTARGET->floating() && m_dragMode == MBIND_MOVE) { + Vector2D MINSIZE = DRAGGINGTARGET->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}); + DRAGGINGTARGET->rememberFloatingSize((DRAGGINGTARGET->position().size() * 0.8489).clamp(MINSIZE, Vector2D{}).floor()); + DRAGGINGTARGET->setPositionGlobal(CBox{g_pInputManager->getMouseCoordsInternal() - DRAGGINGTARGET->position().size() / 2.F, DRAGGINGTARGET->position().size()}); + + if (m_dragThresholdReached) { + g_layoutManager->changeFloatingMode(DRAGGINGTARGET); + m_draggingTiled = true; + } + } + + const auto DRAG_ORIGINAL_BOX = DRAGGINGTARGET->position(); + + m_beginDragXY = g_pInputManager->getMouseCoordsInternal(); + m_beginDragPositionXY = DRAG_ORIGINAL_BOX.pos(); + m_beginDragSizeXY = DRAG_ORIGINAL_BOX.size(); + m_lastDragXY = m_beginDragXY; + + return false; +} + +void CDragStateController::dragBegin(SP target, eMouseBindMode mode) { + m_target = target; + m_dragMode = mode; + + const auto DRAGGINGTARGET = m_target.lock(); + static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); + + m_mouseMoveEventCount = 1; + m_beginDragSizeXY = Vector2D(); + + // Window will be floating. Let's check if it's valid. It should be, but I don't like crashing. + if (!validMapped(DRAGGINGTARGET->window())) { + Log::logger->log(Log::ERR, "Dragging attempted on an invalid window (not mapped)"); + CKeybindManager::changeMouseBindMode(MBIND_INVALID); + return; + } + + if (!DRAGGINGTARGET->workspace()) { + Log::logger->log(Log::ERR, "Dragging attempted on an invalid window (no workspace)"); + CKeybindManager::changeMouseBindMode(MBIND_INVALID); + return; + } + + // Try to pick up dragged window now if drag_threshold is disabled + // or at least update dragging related variables for the cursors + m_dragThresholdReached = *PDRAGTHRESHOLD <= 0; + if (updateDragWindow()) + return; + + // get the grab corner + static auto RESIZECORNER = CConfigValue("general:resize_corner"); + if (*RESIZECORNER != 0 && *RESIZECORNER <= 4 && DRAGGINGTARGET->floating()) { + switch (*RESIZECORNER) { + case 1: + m_grabbedCorner = CORNER_TOPLEFT; + Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + break; + case 2: + m_grabbedCorner = CORNER_TOPRIGHT; + Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + break; + case 3: + m_grabbedCorner = CORNER_BOTTOMRIGHT; + Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + break; + case 4: + m_grabbedCorner = CORNER_BOTTOMLEFT; + Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + break; + } + } else if (m_beginDragXY.x < m_beginDragPositionXY.x + m_beginDragSizeXY.x / 2.F) { + if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.F) { + m_grabbedCorner = CORNER_TOPLEFT; + Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + } else { + m_grabbedCorner = CORNER_BOTTOMLEFT; + Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + } + } else { + if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.F) { + m_grabbedCorner = CORNER_TOPRIGHT; + Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + } else { + m_grabbedCorner = CORNER_BOTTOMRIGHT; + Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + } + } + + if (m_dragMode != MBIND_RESIZE && m_dragMode != MBIND_RESIZE_FORCE_RATIO && m_dragMode != MBIND_RESIZE_BLOCK_RATIO) + Cursor::overrideController->setOverride("grabbing", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + + DRAGGINGTARGET->damageEntire(); + + g_pKeybindManager->shadowKeybinds(); + + if (DRAGGINGTARGET->window()) { + Desktop::focusState()->rawWindowFocus(DRAGGINGTARGET->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); + g_pCompositor->changeWindowZOrder(DRAGGINGTARGET->window(), true); + } +} +void CDragStateController::dragEnd() { + auto draggingTarget = m_target.lock(); + + m_mouseMoveEventCount = 1; + + if (!validMapped(draggingTarget->window())) { + if (draggingTarget->window()) { + Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + m_target.reset(); + } + return; + } + + Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION); + m_target.reset(); + m_wasDraggingWindow = true; + + if (m_dragMode == MBIND_MOVE && draggingTarget->window()) { + draggingTarget->damageEntire(); + + const auto DRAGGING_WINDOW = draggingTarget->window(); + + const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); + PHLWINDOW pWindow = + g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGING_WINDOW); + + if (pWindow) { + if (pWindow->checkInputOnDecos(INPUT_TYPE_DRAG_END, MOUSECOORDS, DRAGGING_WINDOW)) + return; + + const bool FLOATEDINTOTILED = !pWindow->m_isFloating && !m_draggingTiled; + static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); + + if (pWindow->m_group && DRAGGING_WINDOW->canBeGroupedInto(pWindow->m_group) && *PDRAGINTOGROUP == 1 && !FLOATEDINTOTILED) { + pWindow->m_group->add(DRAGGING_WINDOW); + // fix the draggingTarget, now it's DRAGGING_WINDOW + draggingTarget = DRAGGING_WINDOW->m_target; + } + } + } + + if (m_draggingTiled) { + // static auto PPRECISEMOUSE = CConfigValue("dwindle:precise_mouse_move"); + + // FIXME: remove or rethink + // if (*PPRECISEMOUSE) { + // eDirection direction = DIRECTION_DEFAULT; + + // const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); + // const PHLWINDOW pReferenceWindow = + // g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGINGWINDOW); + + // if (pReferenceWindow && pReferenceWindow != DRAGGINGWINDOW) { + // const Vector2D draggedCenter = DRAGGINGWINDOW->m_realPosition->goal() + DRAGGINGWINDOW->m_realSize->goal() / 2.f; + // const Vector2D referenceCenter = pReferenceWindow->m_realPosition->goal() + pReferenceWindow->m_realSize->goal() / 2.f; + // const float xDiff = draggedCenter.x - referenceCenter.x; + // const float yDiff = draggedCenter.y - referenceCenter.y; + + // if (fabs(xDiff) > fabs(yDiff)) + // direction = xDiff < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + // else + // direction = yDiff < 0 ? DIRECTION_UP : DIRECTION_DOWN; + // } + + // onWindowRemovedTiling(DRAGGINGWINDOW); + // onWindowCreatedTiling(DRAGGINGWINDOW, direction); + // } else + + // make sure to check if we are floating because drag into group could make us tiled already + if (draggingTarget->floating()) + g_layoutManager->changeFloatingMode(draggingTarget); + + draggingTarget->rememberFloatingSize(m_draggingWindowOriginalFloatSize); + } + + draggingTarget->damageEntire(); + + Desktop::focusState()->fullWindowFocus(draggingTarget->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE); + + m_wasDraggingWindow = false; + m_dragMode = MBIND_INVALID; +} + +void CDragStateController::mouseMove(const Vector2D& mousePos) { + if (m_target.expired()) + return; + + const auto DRAGGINGTARGET = m_target.lock(); + static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); + + // Window invalid or drag begin size 0,0 meaning we rejected it. + if ((!validMapped(DRAGGINGTARGET->window()) || m_beginDragSizeXY == Vector2D())) { + CKeybindManager::changeMouseBindMode(MBIND_INVALID); + return; + } + + // Yoink dragged window here instead if using drag_threshold and it has been reached + if (*PDRAGTHRESHOLD > 0 && !m_dragThresholdReached) { + if ((m_beginDragXY.distanceSq(mousePos) <= std::pow(*PDRAGTHRESHOLD, 2) && m_beginDragXY == m_lastDragXY)) + return; + m_dragThresholdReached = true; + if (updateDragWindow()) + return; + } + + static auto TIMER = std::chrono::high_resolution_clock::now(), MSTIMER = TIMER; + + const auto DELTA = Vector2D(mousePos.x - m_beginDragXY.x, mousePos.y - m_beginDragXY.y); + const auto TICKDELTA = Vector2D(mousePos.x - m_lastDragXY.x, mousePos.y - m_lastDragXY.y); + + static auto SNAPENABLED = CConfigValue("general:snap:enabled"); + + const auto TIMERDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - TIMER).count(); + const auto MSDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - MSTIMER).count(); + const auto MSMONITOR = 1000.0 / g_pHyprRenderer->m_mostHzMonitor->m_refreshRate; + static int totalMs = 0; + bool canSkipUpdate = true; + + MSTIMER = std::chrono::high_resolution_clock::now(); + + if (m_mouseMoveEventCount == 1) + totalMs = 0; + + if (MSMONITOR > 16.0) { + totalMs += MSDELTA < MSMONITOR ? MSDELTA : std::round(totalMs * 1.0 / m_mouseMoveEventCount); + m_mouseMoveEventCount += 1; + + // check if time-window is enough to skip update on 60hz monitor + canSkipUpdate = std::clamp(MSMONITOR - TIMERDELTA, 0.0, MSMONITOR) > totalMs * 1.0 / m_mouseMoveEventCount; + } + + if ((abs(TICKDELTA.x) < 1.f && abs(TICKDELTA.y) < 1.f) || (TIMERDELTA < MSMONITOR && canSkipUpdate && (m_dragMode != MBIND_MOVE))) + return; + + TIMER = std::chrono::high_resolution_clock::now(); + + m_lastDragXY = mousePos; + + DRAGGINGTARGET->damageEntire(); + + if (m_dragMode == MBIND_MOVE) { + + Vector2D newPos = m_beginDragPositionXY + DELTA; + Vector2D newSize = DRAGGINGTARGET->position().size(); + + if (*SNAPENABLED && !m_draggingTiled) + g_layoutManager->performSnap(newPos, newSize, DRAGGINGTARGET, MBIND_MOVE, -1, m_beginDragSizeXY); + + newPos = newPos.round(); + + DRAGGINGTARGET->setPositionGlobal({newPos, newSize}); + DRAGGINGTARGET->warpPositionSize(); + } else if (m_dragMode == MBIND_RESIZE || m_dragMode == MBIND_RESIZE_FORCE_RATIO || m_dragMode == MBIND_RESIZE_BLOCK_RATIO) { + if (DRAGGINGTARGET->floating()) { + + Vector2D MINSIZE = DRAGGINGTARGET->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}); + Vector2D MAXSIZE = DRAGGINGTARGET->maxSize().value_or(Math::VECTOR2D_MAX); + + Vector2D newSize = m_beginDragSizeXY; + Vector2D newPos = m_beginDragPositionXY; + + if (m_grabbedCorner == CORNER_BOTTOMRIGHT) + newSize = newSize + DELTA; + else if (m_grabbedCorner == CORNER_TOPLEFT) + newSize = newSize - DELTA; + else if (m_grabbedCorner == CORNER_TOPRIGHT) + newSize = newSize + Vector2D(DELTA.x, -DELTA.y); + else if (m_grabbedCorner == CORNER_BOTTOMLEFT) + newSize = newSize + Vector2D(-DELTA.x, DELTA.y); + + eMouseBindMode mode = m_dragMode; + if (DRAGGINGTARGET->window() && DRAGGINGTARGET->window()->m_ruleApplicator->keepAspectRatio().valueOrDefault() && mode != MBIND_RESIZE_BLOCK_RATIO) + mode = MBIND_RESIZE_FORCE_RATIO; + + if (m_beginDragSizeXY.x >= 1 && m_beginDragSizeXY.y >= 1 && mode == MBIND_RESIZE_FORCE_RATIO) { + + const float RATIO = m_beginDragSizeXY.y / m_beginDragSizeXY.x; + + if (MINSIZE.x * RATIO > MINSIZE.y) + MINSIZE = Vector2D(MINSIZE.x, MINSIZE.x * RATIO); + else + MINSIZE = Vector2D(MINSIZE.y / RATIO, MINSIZE.y); + + if (MAXSIZE.x * RATIO < MAXSIZE.y) + MAXSIZE = Vector2D(MAXSIZE.x, MAXSIZE.x * RATIO); + else + MAXSIZE = Vector2D(MAXSIZE.y / RATIO, MAXSIZE.y); + + if (newSize.x * RATIO > newSize.y) + newSize = Vector2D(newSize.x, newSize.x * RATIO); + else + newSize = Vector2D(newSize.y / RATIO, newSize.y); + } + + newSize = newSize.clamp(MINSIZE, MAXSIZE); + + if (m_grabbedCorner == CORNER_TOPLEFT) + newPos = newPos - newSize + m_beginDragSizeXY; + else if (m_grabbedCorner == CORNER_TOPRIGHT) + newPos = newPos + Vector2D(0.0, (m_beginDragSizeXY - newSize).y); + else if (m_grabbedCorner == CORNER_BOTTOMLEFT) + newPos = newPos + Vector2D((m_beginDragSizeXY - newSize).x, 0.0); + + if (*SNAPENABLED) { + g_layoutManager->performSnap(newPos, newSize, DRAGGINGTARGET, mode, m_grabbedCorner, m_beginDragSizeXY); + newSize = newSize.clamp(MINSIZE, MAXSIZE); + } + + CBox wb = {newPos, newSize}; + wb.round(); + + DRAGGINGTARGET->setPositionGlobal(wb); + DRAGGINGTARGET->warpPositionSize(); + } else { + g_layoutManager->resizeTarget(TICKDELTA, DRAGGINGTARGET, m_grabbedCorner); + DRAGGINGTARGET->warpPositionSize(); + } + } + + // get middle point + Vector2D middle = DRAGGINGTARGET->position().middle(); + + // and check its monitor + const auto PMONITOR = g_pCompositor->getMonitorFromVector(middle); + + if (PMONITOR && PMONITOR->m_activeWorkspace && DRAGGINGTARGET->floating() /* If we're resaizing a tiled target, don't do this */) { + const auto WS = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace; + DRAGGINGTARGET->assignToSpace(WS->m_space); + } + + DRAGGINGTARGET->damageEntire(); +} diff --git a/src/layout/supplementary/DragController.hpp b/src/layout/supplementary/DragController.hpp new file mode 100644 index 00000000..3a0d8071 --- /dev/null +++ b/src/layout/supplementary/DragController.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "../target/Target.hpp" +#include "../../managers/input/InputManager.hpp" + +namespace Layout { + enum eRectCorner : uint8_t; +} + +namespace Layout::Supplementary { + + // DragStateController contains logic to begin and end a drag, which shouldn't be part of the layout's job. It's stuff like + // toggling float when dragging tiled, remembering sizes, checking deltas, etc. + class CDragStateController { + public: + CDragStateController() = default; + ~CDragStateController() = default; + + void dragBegin(SP target, eMouseBindMode mode); + void dragEnd(); + + void mouseMove(const Vector2D& mousePos); + eMouseBindMode mode() const; + bool wasDraggingWindow() const; + bool dragThresholdReached() const; + void resetDragThresholdReached(); + bool draggingTiled() const; + + /* + Called to try to pick up window for dragging. + Updates drag related variables and floats window if threshold reached. + Return true to reject + */ + bool updateDragWindow(); + + SP target() const; + + private: + WP m_target; + + eMouseBindMode m_dragMode = MBIND_INVALID; + bool m_wasDraggingWindow = false; + bool m_dragThresholdReached = false; + bool m_draggingTiled = false; + + int m_mouseMoveEventCount = 0; + Vector2D m_beginDragXY; + Vector2D m_lastDragXY; + Vector2D m_beginDragPositionXY; + Vector2D m_beginDragSizeXY; + Vector2D m_draggingWindowOriginalFloatSize; + Layout::eRectCorner m_grabbedCorner = sc(0) /* CORNER_NONE */; + }; +}; diff --git a/src/layout/supplementary/WorkspaceAlgoMatcher.cpp b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp new file mode 100644 index 00000000..b476c3a0 --- /dev/null +++ b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp @@ -0,0 +1,139 @@ +#include "WorkspaceAlgoMatcher.hpp" + +#include "../../config/ConfigValue.hpp" +#include "../../config/ConfigManager.hpp" + +#include "../algorithm/Algorithm.hpp" +#include "../space/Space.hpp" + +#include "../algorithm/floating/default/DefaultFloatingAlgorithm.hpp" +#include "../algorithm/tiled/dwindle/DwindleAlgorithm.hpp" +#include "../algorithm/tiled/master/MasterAlgorithm.hpp" +#include "../algorithm/tiled/scrolling/ScrollingAlgorithm.hpp" +#include "../algorithm/tiled/monocle/MonocleAlgorithm.hpp" + +#include "../../Compositor.hpp" + +using namespace Layout; +using namespace Layout::Supplementary; + +constexpr const char* DEFAULT_FLOATING_ALGO = "default"; +constexpr const char* DEFAULT_TILED_ALGO = "dwindle"; + +const UP& Supplementary::algoMatcher() { + static UP m = makeUnique(); + return m; +} + +CWorkspaceAlgoMatcher::CWorkspaceAlgoMatcher() { + m_tiledAlgos = { + {"dwindle", [] { return makeUnique(); }}, + {"master", [] { return makeUnique(); }}, + {"scrolling", [] { return makeUnique(); }}, + {"monocle", [] { return makeUnique(); }}, + }; + + m_floatingAlgos = { + {"default", [] { return makeUnique(); }}, + }; + + m_algoNames = { + {&typeid(Tiled::CDwindleAlgorithm), "dwindle"}, + {&typeid(Tiled::CMasterAlgorithm), "master"}, + {&typeid(Tiled::CScrollingAlgorithm), "scrolling"}, + {&typeid(Tiled::CMonocleAlgorithm), "monocle"}, + {&typeid(Floating::CDefaultFloatingAlgorithm), "default"}, + }; +} + +bool CWorkspaceAlgoMatcher::registerTiledAlgo(const std::string& name, const std::type_info* typeInfo, std::function()>&& factory) { + if (m_tiledAlgos.contains(name) || m_floatingAlgos.contains(name)) + return false; + + m_tiledAlgos.emplace(name, std::move(factory)); + m_algoNames.emplace(typeInfo, name); + + updateWorkspaceLayouts(); + + return true; +} + +bool CWorkspaceAlgoMatcher::registerFloatingAlgo(const std::string& name, const std::type_info* typeInfo, std::function()>&& factory) { + if (m_tiledAlgos.contains(name) || m_floatingAlgos.contains(name)) + return false; + + m_floatingAlgos.emplace(name, std::move(factory)); + m_algoNames.emplace(typeInfo, name); + + updateWorkspaceLayouts(); + + return true; +} + +bool CWorkspaceAlgoMatcher::unregisterAlgo(const std::string& name) { + if (!m_tiledAlgos.contains(name) && !m_floatingAlgos.contains(name)) + return false; + + std::erase_if(m_algoNames, [&name](const auto& e) { return e.second == name; }); + + if (m_floatingAlgos.contains(name)) + m_floatingAlgos.erase(name); + else + m_tiledAlgos.erase(name); + + // this is needed here to avoid situations where a plugin unloads and we still have a UP + // to a plugin layout + updateWorkspaceLayouts(); + + return true; +} + +UP CWorkspaceAlgoMatcher::algoForNameTiled(const std::string& s) { + if (m_tiledAlgos.contains(s)) + return m_tiledAlgos.at(s)(); + return m_tiledAlgos.at(DEFAULT_TILED_ALGO)(); +} + +UP CWorkspaceAlgoMatcher::algoForNameFloat(const std::string& s) { + if (m_floatingAlgos.contains(s)) + return m_floatingAlgos.at(s)(); + return m_floatingAlgos.at(DEFAULT_FLOATING_ALGO)(); +} + +std::string CWorkspaceAlgoMatcher::tiledAlgoForWorkspace(const PHLWORKSPACE& w) { + static auto PLAYOUT = CConfigValue("general:layout"); + + auto rule = g_pConfigManager->getWorkspaceRuleFor(w); + return rule.layout.value_or(*PLAYOUT); +} + +SP CWorkspaceAlgoMatcher::createAlgorithmForWorkspace(PHLWORKSPACE w) { + return CAlgorithm::create(algoForNameTiled(tiledAlgoForWorkspace(w)), makeUnique(), w->m_space); +} + +void CWorkspaceAlgoMatcher::updateWorkspaceLayouts() { + // TODO: make this ID-based, string comparison is slow + for (const auto& ws : g_pCompositor->getWorkspaces()) { + if (!ws) + continue; + + const auto& TILED_ALGO = ws->m_space->algorithm()->tiledAlgo(); + + if (!TILED_ALGO) + continue; + + const auto LAYOUT_TO_USE = tiledAlgoForWorkspace(ws.lock()); + + if (m_algoNames.contains(&typeid(*TILED_ALGO.get())) && m_algoNames.at(&typeid(*TILED_ALGO.get())) == LAYOUT_TO_USE) + continue; + + // needs a switchup + ws->m_space->algorithm()->updateTiledAlgo(algoForNameTiled(LAYOUT_TO_USE)); + } +} + +std::string CWorkspaceAlgoMatcher::getNameForTiledAlgo(const std::type_info* type) { + if (m_algoNames.contains(type)) + return m_algoNames.at(type); + return "unknown"; +} diff --git a/src/layout/supplementary/WorkspaceAlgoMatcher.hpp b/src/layout/supplementary/WorkspaceAlgoMatcher.hpp new file mode 100644 index 00000000..d39e2998 --- /dev/null +++ b/src/layout/supplementary/WorkspaceAlgoMatcher.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "../../desktop/DesktopTypes.hpp" + +#include +#include +#include +#include + +namespace Layout { + class CAlgorithm; + class ITiledAlgorithm; + class IFloatingAlgorithm; +} + +namespace Layout::Supplementary { + class CWorkspaceAlgoMatcher { + public: + CWorkspaceAlgoMatcher(); + ~CWorkspaceAlgoMatcher() = default; + + SP createAlgorithmForWorkspace(PHLWORKSPACE w); + void updateWorkspaceLayouts(); + std::string getNameForTiledAlgo(const std::type_info* type); + + // these fns can fail due to name collisions + bool registerTiledAlgo(const std::string& name, const std::type_info* typeInfo, std::function()>&& factory); + bool registerFloatingAlgo(const std::string& name, const std::type_info* typeInfo, std::function()>&& factory); + + // this fn fails if the algo isn't registered + bool unregisterAlgo(const std::string& name); + + private: + UP algoForNameTiled(const std::string& s); + UP algoForNameFloat(const std::string& s); + + std::string tiledAlgoForWorkspace(const PHLWORKSPACE&); + + std::map()>> m_tiledAlgos; + std::map()>> m_floatingAlgos; + + std::map m_algoNames; + }; + + const UP& algoMatcher(); +} \ No newline at end of file diff --git a/src/layout/target/Target.cpp b/src/layout/target/Target.cpp new file mode 100644 index 00000000..e433c237 --- /dev/null +++ b/src/layout/target/Target.cpp @@ -0,0 +1,146 @@ +#include "Target.hpp" +#include "../space/Space.hpp" +#include "../../debug/log/Logger.hpp" + +#include + +using namespace Layout; +using namespace Hyprutils::Utils; + +void ITarget::setPositionGlobal(const CBox& box) { + m_box = box; + m_box.round(); +} + +void ITarget::assignToSpace(const SP& space, std::optional focalPoint) { + if (m_space == space && !m_ghostSpace) + return; + + const bool HAD_SPACE = !!m_space; + + if (m_space && !m_ghostSpace) + m_space->remove(m_self.lock()); + + m_space = space; + + if (space && HAD_SPACE) + space->move(m_self.lock(), focalPoint); + else if (space) + space->add(m_self.lock()); + + if (!space) + Log::logger->log(Log::WARN, "ITarget: assignToSpace with a null space?"); + + m_ghostSpace = false; + + onUpdateSpace(); +} + +void ITarget::setSpaceGhost(const SP& space) { + if (m_space) + assignToSpace(nullptr); + + m_space = space; + + m_ghostSpace = true; +} + +SP ITarget::space() const { + return m_space; +} + +PHLWORKSPACE ITarget::workspace() const { + if (!m_space) + return nullptr; + + return m_space->workspace(); +} + +CBox ITarget::position() const { + return m_box; +} + +void ITarget::rememberFloatingSize(const Vector2D& size) { + m_floatingSize = size; +} + +Vector2D ITarget::lastFloatingSize() const { + return m_floatingSize; +} + +void ITarget::recalc() { + setPositionGlobal(m_box); +} + +void ITarget::setPseudo(bool x) { + if (m_pseudo == x) + return; + + m_pseudo = x; + + recalc(); +} + +bool ITarget::isPseudo() const { + return m_pseudo; +} + +void ITarget::setPseudoSize(const Vector2D& size) { + m_pseudoSize = size; + + recalc(); +} + +Vector2D ITarget::pseudoSize() { + return m_pseudoSize; +} + +void ITarget::swap(SP b) { + const auto IS_FLOATING = floating(); + const auto IS_FLOATING_B = b->floating(); + + // Keep workspaces alive during a swap: moving one window will unref the ws + + // NOLINTNEXTLINE + const auto PWS1 = workspace(); + // NOLINTNEXTLINE + const auto PWS2 = b->workspace(); + + CScopeGuard x([&] { + b->setFloating(IS_FLOATING); + setFloating(IS_FLOATING_B); + + // update the spaces + b->onUpdateSpace(); + onUpdateSpace(); + }); + + if (b->space() == m_space) { + // simplest + m_space->swap(m_self.lock(), b); + m_space->recalculate(); + return; + } + + // spaces differ + if (m_space) + m_space->swap(m_self.lock(), b); + if (b->space()) + b->space()->swap(b, m_self.lock()); + + std::swap(m_space, b->m_space); + + // recalc both + if (m_space) + m_space->recalculate(); + if (b->space()) + b->space()->recalculate(); +} + +bool ITarget::wasTiling() const { + return m_wasTiling; +} + +void ITarget::setWasTiling(bool x) { + m_wasTiling = x; +} diff --git a/src/layout/target/Target.hpp b/src/layout/target/Target.hpp new file mode 100644 index 00000000..dcaefdb4 --- /dev/null +++ b/src/layout/target/Target.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "../../helpers/math/Math.hpp" +#include "../../helpers/memory/Memory.hpp" +#include "../../desktop/Workspace.hpp" + +#include +#include + +namespace Layout { + enum eTargetType : uint8_t { + TARGET_TYPE_WINDOW = 0, + TARGET_TYPE_GROUP, + }; + + enum eGeometryFailure : uint8_t { + GEOMETRY_NO_DESIRED = 0, + GEOMETRY_INVALID_DESIRED = 1, + }; + + class CSpace; + + struct SGeometryRequested { + Vector2D size; + std::optional pos; + }; + + class ITarget { + public: + virtual ~ITarget() = default; + + virtual eTargetType type() = 0; + + // position is within its space + virtual void setPositionGlobal(const CBox& box); + virtual CBox position() const; + virtual void assignToSpace(const SP& space, std::optional focalPoint = std::nullopt); + virtual void setSpaceGhost(const SP& space); + virtual SP space() const; + virtual PHLWORKSPACE workspace() const; + virtual PHLWINDOW window() const = 0; + virtual void recalc(); + virtual bool wasTiling() const; + virtual void setWasTiling(bool x); + + virtual void rememberFloatingSize(const Vector2D& size); + virtual Vector2D lastFloatingSize() const; + + virtual void setPseudo(bool x); + virtual bool isPseudo() const; + virtual void setPseudoSize(const Vector2D& size); + virtual Vector2D pseudoSize(); + virtual void swap(SP b); + + // + virtual bool floating() = 0; + virtual void setFloating(bool x) = 0; + virtual std::expected desiredGeometry() = 0; + virtual eFullscreenMode fullscreenMode() = 0; + virtual void setFullscreenMode(eFullscreenMode mode) = 0; + virtual std::optional minSize() = 0; + virtual std::optional maxSize() = 0; + virtual void damageEntire() = 0; + virtual void warpPositionSize() = 0; + virtual void onUpdateSpace() = 0; + + protected: + ITarget() = default; + + CBox m_box; + SP m_space; + WP m_self; + Vector2D m_floatingSize; + bool m_pseudo = false; + bool m_ghostSpace = false; // ghost space means a target belongs to a space, but isn't sent to the layout + Vector2D m_pseudoSize = {1280, 720}; + bool m_wasTiling = false; + }; +}; \ No newline at end of file diff --git a/src/layout/target/WindowGroupTarget.cpp b/src/layout/target/WindowGroupTarget.cpp new file mode 100644 index 00000000..ae883751 --- /dev/null +++ b/src/layout/target/WindowGroupTarget.cpp @@ -0,0 +1,92 @@ +#include "WindowGroupTarget.hpp" + +#include "../space/Space.hpp" +#include "../algorithm/Algorithm.hpp" +#include "WindowTarget.hpp" +#include "Target.hpp" + +#include "../../render/Renderer.hpp" + +using namespace Layout; + +SP CWindowGroupTarget::create(SP g) { + auto target = SP(new CWindowGroupTarget(g)); + target->m_self = target; + return target; +} + +CWindowGroupTarget::CWindowGroupTarget(SP g) : m_group(g) { + ; +} + +eTargetType CWindowGroupTarget::type() { + return TARGET_TYPE_GROUP; +} + +void CWindowGroupTarget::setPositionGlobal(const CBox& box) { + ITarget::setPositionGlobal(box); + + updatePos(); +} + +void CWindowGroupTarget::updatePos() { + for (const auto& w : m_group->windows()) { + w->m_target->setPositionGlobal(m_box); + } +} + +void CWindowGroupTarget::assignToSpace(const SP& space, std::optional focalPoint) { + ITarget::assignToSpace(space, focalPoint); + + m_group->updateWorkspace(space->workspace()); +} + +bool CWindowGroupTarget::floating() { + return m_group->current()->m_target->floating(); +} + +void CWindowGroupTarget::setFloating(bool x) { + for (const auto& w : m_group->windows()) { + w->m_target->setFloating(x); + } +} + +std::expected CWindowGroupTarget::desiredGeometry() { + return m_group->current()->m_target->desiredGeometry(); +} + +PHLWINDOW CWindowGroupTarget::window() const { + return m_group->current(); +} + +eFullscreenMode CWindowGroupTarget::fullscreenMode() { + return m_group->current()->m_fullscreenState.internal; +} + +void CWindowGroupTarget::setFullscreenMode(eFullscreenMode mode) { + m_group->current()->m_fullscreenState.internal = mode; +} + +std::optional CWindowGroupTarget::minSize() { + return m_group->current()->minSize(); +} + +std::optional CWindowGroupTarget::maxSize() { + return m_group->current()->maxSize(); +} + +void CWindowGroupTarget::damageEntire() { + g_pHyprRenderer->damageWindow(m_group->current()); +} + +void CWindowGroupTarget::warpPositionSize() { + for (const auto& w : m_group->windows()) { + w->m_target->warpPositionSize(); + } +} + +void CWindowGroupTarget::onUpdateSpace() { + for (const auto& w : m_group->windows()) { + w->m_target->onUpdateSpace(); + } +} diff --git a/src/layout/target/WindowGroupTarget.hpp b/src/layout/target/WindowGroupTarget.hpp new file mode 100644 index 00000000..3d4b85a0 --- /dev/null +++ b/src/layout/target/WindowGroupTarget.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "Target.hpp" + +#include "../../desktop/view/Window.hpp" +#include "../../desktop/view/Group.hpp" + +namespace Layout { + + class CWindowGroupTarget : public ITarget { + public: + static SP create(SP g); + virtual ~CWindowGroupTarget() = default; + + virtual eTargetType type(); + + virtual void setPositionGlobal(const CBox& box); + virtual void assignToSpace(const SP& space, std::optional focalPoint = std::nullopt); + virtual PHLWINDOW window() const; + + virtual bool floating(); + virtual void setFloating(bool x); + virtual std::expected desiredGeometry(); + virtual eFullscreenMode fullscreenMode(); + virtual void setFullscreenMode(eFullscreenMode mode); + virtual std::optional minSize(); + virtual std::optional maxSize(); + virtual void damageEntire(); + virtual void warpPositionSize(); + virtual void onUpdateSpace(); + + private: + CWindowGroupTarget(SP g); + + void updatePos(); + + WP m_group; + }; +}; \ No newline at end of file diff --git a/src/layout/target/WindowTarget.cpp b/src/layout/target/WindowTarget.cpp new file mode 100644 index 00000000..0bd905af --- /dev/null +++ b/src/layout/target/WindowTarget.cpp @@ -0,0 +1,363 @@ +#include "WindowTarget.hpp" + +#include "../space/Space.hpp" +#include "../algorithm/Algorithm.hpp" + +#include "../../protocols/core/Compositor.hpp" +#include "../../config/ConfigManager.hpp" +#include "../../helpers/Monitor.hpp" +#include "../../xwayland/XSurface.hpp" +#include "../../Compositor.hpp" +#include "../../render/Renderer.hpp" + +using namespace Layout; + +SP CWindowTarget::create(PHLWINDOW w) { + auto target = SP(new CWindowTarget(w)); + target->m_self = target; + return target; +} + +CWindowTarget::CWindowTarget(PHLWINDOW w) : m_window(w) { + ; +} + +eTargetType CWindowTarget::type() { + return TARGET_TYPE_WINDOW; +} + +void CWindowTarget::setPositionGlobal(const CBox& box) { + ITarget::setPositionGlobal(box); + + updatePos(); +} + +void CWindowTarget::updatePos() { + + if (!m_space) + return; + + if (fullscreenMode() == FSMODE_FULLSCREEN) + return; + + if (floating() && fullscreenMode() != FSMODE_MAXIMIZED) { + m_window->m_position = m_box.pos(); + m_window->m_size = m_box.size(); + + *m_window->m_realPosition = m_box.pos(); + *m_window->m_realSize = m_box.size(); + + m_window->sendWindowSize(); + m_window->updateWindowDecos(); + + return; + } + + // Tiled is more complicated. + + const auto PMONITOR = m_space->workspace()->m_monitor; + const auto PWORKSPACE = m_space->workspace(); + + // for gaps outer + const auto MONITOR_WORKAREA = m_space->workArea(); + const bool DISPLAYLEFT = STICKS(m_box.x, MONITOR_WORKAREA.x); + const bool DISPLAYRIGHT = STICKS(m_box.x + m_box.w, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); + const bool DISPLAYTOP = STICKS(m_box.y, MONITOR_WORKAREA.y); + const bool DISPLAYBOTTOM = STICKS(m_box.y + m_box.h, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h); + + // this is used for scrolling, so that the gaps are correct when a window is the full width and has neighbors + const bool DISPLAYINVERSELEFT = STICKS(m_box.x, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); + const bool DISPLAYINVERSERIGHT = STICKS(m_box.x + m_box.w, MONITOR_WORKAREA.x); + + // get specific gaps and rules for this workspace, + // if user specified them in config + const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(PWORKSPACE); + + if (!validMapped(m_window)) { + if (m_window) + g_layoutManager->removeTarget(m_window->layoutTarget()); + return; + } + + if (fullscreenMode() == FSMODE_FULLSCREEN) + return; + + g_pHyprRenderer->damageWindow(window()); + + static auto PGAPSINDATA = CConfigValue("general:gaps_in"); + auto* const PGAPSIN = sc((PGAPSINDATA.ptr())->getData()); + + auto gapsIn = WORKSPACERULE.gapsIn.value_or(*PGAPSIN); + CBox nodeBox = m_box; + nodeBox.round(); + + m_window->m_size = nodeBox.size(); + m_window->m_position = nodeBox.pos(); + + m_window->updateWindowDecos(); + + auto calcPos = m_window->m_position; + auto calcSize = m_window->m_size; + + const static auto REQUESTEDRATIO = CConfigValue("layout:single_window_aspect_ratio"); + const static auto REQUESTEDRATIOTOLERANCE = CConfigValue("layout:single_window_aspect_ratio_tolerance"); + + Vector2D ratioPadding; + + if ((*REQUESTEDRATIO).y != 0 && m_space->algorithm()->tiledTargets() <= 1) { + const Vector2D originalSize = MONITOR_WORKAREA.size(); + + const double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y; + const double originalRatio = originalSize.x / originalSize.y; + + if (requestedRatio > originalRatio) { + double padding = originalSize.y - (originalSize.x / requestedRatio); + + if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.y) + ratioPadding = Vector2D{0., padding}; + } else if (requestedRatio < originalRatio) { + double padding = originalSize.x - (originalSize.y * requestedRatio); + + if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.x) + ratioPadding = Vector2D{padding, 0.}; + } + } + + const auto GAPOFFSETTOPLEFT = Vector2D(sc(DISPLAYLEFT ? 0 : (DISPLAYINVERSELEFT ? 2 * gapsIn.m_left : gapsIn.m_left)), sc(DISPLAYTOP ? 0 : gapsIn.m_top)); + + const auto GAPOFFSETBOTTOMRIGHT = + Vector2D(sc(DISPLAYRIGHT ? 0 : (DISPLAYINVERSERIGHT ? 2 * gapsIn.m_right : gapsIn.m_right)), sc(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom)); + + calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2; + calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding; + + if (isPseudo()) { + // Calculate pseudo + float scale = 1; + + // adjust if doesn't fit + if (m_pseudoSize.x > calcSize.x || m_pseudoSize.y > calcSize.y) { + if (m_pseudoSize.x > calcSize.x) + scale = calcSize.x / m_pseudoSize.x; + + if (m_pseudoSize.y * scale > calcSize.y) + scale = calcSize.y / m_pseudoSize.y; + + auto DELTA = calcSize - m_pseudoSize * scale; + calcSize = m_pseudoSize * scale; + calcPos = calcPos + DELTA / 2.f; // center + } else { + auto DELTA = calcSize - m_pseudoSize; + calcPos = calcPos + DELTA / 2.f; // center + calcSize = m_pseudoSize; + } + } + + const auto RESERVED = m_window->getFullWindowReservedArea(); + calcPos = calcPos + RESERVED.topLeft; + calcSize = calcSize - (RESERVED.topLeft + RESERVED.bottomRight); + + Vector2D availableSpace = calcSize; + + static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); + + if (*PCLAMP_TILED) { + const auto borderSize = m_window->getRealBorderSize(); + Vector2D monitorAvailable = MONITOR_WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize}; + + Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}).clamp(Vector2D{0, 0}, monitorAvailable); + Vector2D maxSize = m_window->isFullscreen() ? Vector2D{INFINITY, INFINITY} : + m_window->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}).clamp(Vector2D{0, 0}, monitorAvailable); + calcSize = calcSize.clamp(minSize, maxSize); + + calcPos += (availableSpace - calcSize) / 2.0; + + calcPos.x = std::clamp(calcPos.x, MONITOR_WORKAREA.x + borderSize, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w - calcSize.x - borderSize); + calcPos.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y + borderSize, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y - borderSize); + } + + if (m_window->onSpecialWorkspace() && !m_window->isFullscreen()) { + // if special, we adjust the coords a bit + static auto PSCALEFACTOR = CConfigValue("dwindle:special_scale_factor"); + + CBox wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR}; + wb.round(); // avoid rounding mess + + *m_window->m_realPosition = wb.pos(); + *m_window->m_realSize = wb.size(); + } else { + CBox wb = {calcPos, calcSize}; + wb.round(); // avoid rounding mess + + *m_window->m_realSize = wb.size(); + *m_window->m_realPosition = wb.pos(); + } + + m_window->updateWindowDecos(); +} + +void CWindowTarget::assignToSpace(const SP& space, std::optional focalPoint) { + if (!space) { + ITarget::assignToSpace(space, focalPoint); + return; + } + + // keep the ref here so that moveToWorkspace doesn't unref the workspace + // and assignToSpace doesn't think this is a new target because space wp is dead + const auto WSREF = space->workspace(); + + m_window->m_monitor = space->workspace()->m_monitor; + m_window->moveToWorkspace(space->workspace()); + + // layout and various update fns want the target to already have m_workspace set + ITarget::assignToSpace(space, focalPoint); + + m_window->updateToplevel(); + m_window->updateWindowDecos(); +} + +bool CWindowTarget::floating() { + return m_window->m_isFloating; +} + +void CWindowTarget::setFloating(bool x) { + if (x == m_window->m_isFloating) + return; + + m_window->m_isFloating = x; + m_window->m_pinned = false; + + m_window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_FLOATING); +} + +Vector2D CWindowTarget::clampSizeForDesired(const Vector2D& size) const { + Vector2D newSize = size; + if (const auto m = m_window->minSize(); m) + newSize = newSize.clamp(*m); + if (const auto m = m_window->maxSize(); m) + newSize = newSize.clamp(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}, *m); + return newSize; +} + +std::expected CWindowTarget::desiredGeometry() { + + SGeometryRequested requested; + + CBox DESIRED_GEOM = g_pXWaylandManager->getGeometryForWindow(m_window.lock()); + const auto PMONITOR = m_window->m_monitor.lock(); + + requested.size = clampSizeForDesired(DESIRED_GEOM.size()); + + if (m_window->m_isX11) { + Vector2D xy = {DESIRED_GEOM.x, DESIRED_GEOM.y}; + xy = g_pXWaylandManager->xwaylandToWaylandCoords(xy); + requested.pos = xy; + } + + const auto STOREDSIZE = m_window->m_ruleApplicator->persistentSize().valueOrDefault() ? g_pConfigManager->getStoredFloatingSize(m_window.lock()) : std::nullopt; + + if (STOREDSIZE) + requested.size = clampSizeForDesired(*STOREDSIZE); + + if (!PMONITOR) { + Log::logger->log(Log::ERR, "{:m} has an invalid monitor in desiredGeometry!", m_window.lock()); + return std::unexpected(GEOMETRY_NO_DESIRED); + } + + if (DESIRED_GEOM.width <= 2 || DESIRED_GEOM.height <= 2) { + const auto SURFACE = m_window->wlSurface()->resource(); + + if (SURFACE->m_current.size.x > 5 && SURFACE->m_current.size.y > 5) { + // center on mon and call it a day + requested.pos.reset(); + requested.size = clampSizeForDesired(SURFACE->m_current.size); + return requested; + } + + if (m_window->m_isX11 && m_window->isX11OverrideRedirect()) { + // check OR windows, they like their shit + const auto SIZE = clampSizeForDesired(m_window->m_xwaylandSurface->m_geometry.w > 0 && m_window->m_xwaylandSurface->m_geometry.h > 0 ? + m_window->m_xwaylandSurface->m_geometry.size() : + Vector2D{600, 400}); + + if (m_window->m_xwaylandSurface->m_geometry.x != 0 && m_window->m_xwaylandSurface->m_geometry.y != 0) { + requested.size = SIZE; + requested.pos = g_pXWaylandManager->xwaylandToWaylandCoords(m_window->m_xwaylandSurface->m_geometry.pos()); + return requested; + } + } + + return std::unexpected(m_window->m_isX11 && m_window->isX11OverrideRedirect() ? GEOMETRY_INVALID_DESIRED : GEOMETRY_NO_DESIRED); + } + + // TODO: detect a popup in a more consistent way. + if ((DESIRED_GEOM.x == 0 && DESIRED_GEOM.y == 0) || !m_window->m_isX11) { + // middle of parent if available + if (!m_window->m_isX11) { + if (const auto PARENT = m_window->parent(); PARENT) { + const auto POS = PARENT->m_realPosition->goal() + PARENT->m_realSize->goal() / 2.F - DESIRED_GEOM.size() / 2.F; + requested.pos = POS; + } + } + } else { + // if it is, we respect where it wants to put itself, but apply monitor offset if outside + // most of these are popups + + Vector2D pos; + + if (const auto POPENMON = g_pCompositor->getMonitorFromVector(DESIRED_GEOM.middle()); POPENMON->m_id != PMONITOR->m_id) + pos = Vector2D(DESIRED_GEOM.x, DESIRED_GEOM.y) - POPENMON->m_position + PMONITOR->m_position; + else + pos = Vector2D(DESIRED_GEOM.x, DESIRED_GEOM.y); + + requested.pos = pos; + } + + if (DESIRED_GEOM.w <= 2 || DESIRED_GEOM.h <= 2) + return std::unexpected(GEOMETRY_NO_DESIRED); + + return requested; +} + +PHLWINDOW CWindowTarget::window() const { + return m_window.lock(); +} + +eFullscreenMode CWindowTarget::fullscreenMode() { + return m_window->m_fullscreenState.internal; +} + +void CWindowTarget::setFullscreenMode(eFullscreenMode mode) { + if (floating() && m_window->m_fullscreenState.internal == FSMODE_NONE) + rememberFloatingSize(m_box.size()); + + m_window->m_fullscreenState.internal = mode; +} + +std::optional CWindowTarget::minSize() { + return m_window->minSize(); +} + +std::optional CWindowTarget::maxSize() { + return m_window->maxSize(); +} + +void CWindowTarget::damageEntire() { + g_pHyprRenderer->damageWindow(m_window.lock()); +} + +void CWindowTarget::warpPositionSize() { + m_window->m_realSize->warp(); + m_window->m_realPosition->warp(); + m_window->updateWindowDecos(); +} + +void CWindowTarget::onUpdateSpace() { + if (!space()) + return; + + m_window->m_monitor = space()->workspace()->m_monitor; + m_window->moveToWorkspace(space()->workspace()); + m_window->updateToplevel(); + m_window->updateWindowDecos(); +} diff --git a/src/layout/target/WindowTarget.hpp b/src/layout/target/WindowTarget.hpp new file mode 100644 index 00000000..2939fd74 --- /dev/null +++ b/src/layout/target/WindowTarget.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "Target.hpp" + +#include "../../desktop/view/Window.hpp" + +namespace Layout { + + class CWindowTarget : public ITarget { + public: + static SP create(PHLWINDOW w); + virtual ~CWindowTarget() = default; + + virtual eTargetType type(); + + virtual void setPositionGlobal(const CBox& box); + virtual void assignToSpace(const SP& space, std::optional focalPoint = std::nullopt); + virtual PHLWINDOW window() const; + + virtual bool floating(); + virtual void setFloating(bool x); + virtual std::expected desiredGeometry(); + virtual eFullscreenMode fullscreenMode(); + virtual void setFullscreenMode(eFullscreenMode mode); + virtual std::optional minSize(); + virtual std::optional maxSize(); + virtual void damageEntire(); + virtual void warpPositionSize(); + virtual void onUpdateSpace(); + + private: + CWindowTarget(PHLWINDOW w); + + Vector2D clampSizeForDesired(const Vector2D& size) const; + + void updatePos(); + + PHLWINDOWREF m_window; + }; +}; \ No newline at end of file -- cgit v1.2.3