summaryrefslogtreecommitdiff
path: root/src/layout/algorithm
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-02-21 21:30:39 +0000
committerGitHub <noreply@github.com>2026-02-21 21:30:39 +0000
commit723870337f299d4c80aa0048144f3a1ca1c885bd (patch)
treeca3279ae5dbc841f676c64d2f58f895de13cd3bf /src/layout/algorithm
parent51f8849e541f0a8150ac3a43fa928c1e299daf18 (diff)
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.
Diffstat (limited to 'src/layout/algorithm')
-rw-r--r--src/layout/algorithm/Algorithm.cpp264
-rw-r--r--src/layout/algorithm/Algorithm.hpp64
-rw-r--r--src/layout/algorithm/FloatingAlgorithm.cpp18
-rw-r--r--src/layout/algorithm/FloatingAlgorithm.hpp31
-rw-r--r--src/layout/algorithm/ModeAlgorithm.cpp11
-rw-r--r--src/layout/algorithm/ModeAlgorithm.hpp54
-rw-r--r--src/layout/algorithm/TiledAlgorithm.hpp26
-rw-r--r--src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp223
-rw-r--r--src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.hpp26
-rw-r--r--src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp772
-rw-r--r--src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp57
-rw-r--r--src/layout/algorithm/tiled/master/MasterAlgorithm.cpp1292
-rw-r--r--src/layout/algorithm/tiled/master/MasterAlgorithm.hpp75
-rw-r--r--src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp274
-rw-r--r--src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp52
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollTapeController.cpp293
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollTapeController.hpp83
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp1412
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp137
19 files changed, 5164 insertions, 0 deletions
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> CAlgorithm::create(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space) {
+ auto algo = SP<CAlgorithm>(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<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space) :
+ m_tiled(std::move(tiled)), m_floating(std::move(floating)), m_space(space) {
+ ;
+}
+
+void CAlgorithm::addTarget(SP<ITarget> 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<ITarget> 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<ITarget> target, std::optional<Vector2D> 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<CSpace> CAlgorithm::space() const {
+ return m_space.lock();
+}
+
+void CAlgorithm::setFloating(SP<ITarget> 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<ITarget> t) {
+ if (t->floating())
+ m_floating->recenter(t);
+}
+
+std::expected<void, std::string> 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<Vector2D> CAlgorithm::predictSizeForNewTiledTarget() {
+ return m_tiled->predictSizeForNewTarget();
+}
+
+void CAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
+ if (target->floating())
+ m_floating->resizeTarget(Δ, target, corner);
+ else
+ m_tiled->resizeTarget(Δ, target, corner);
+}
+
+void CAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
+ if (target->floating())
+ m_floating->moveTarget(Δ, target);
+}
+
+void CAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
+ auto swapFirst = [&a, &b](std::vector<WP<ITarget>>& 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<IModeAlgorithm> algA = a->floating() ? WP<IModeAlgorithm>(m_floating) : WP<IModeAlgorithm>(m_tiled);
+ const WP<IModeAlgorithm> algB = b->floating() ? WP<IModeAlgorithm>(m_floating) : WP<IModeAlgorithm>(m_tiled);
+
+ algA->swapTargets(a, b);
+ if (algA != algB)
+ algB->swapTargets(b, a);
+}
+
+void CAlgorithm::moveTargetInDirection(SP<ITarget> 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<IFloatingAlgorithm>&& 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<ITiledAlgorithm>&& 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<ITiledAlgorithm>& CAlgorithm::tiledAlgo() const {
+ return m_tiled;
+}
+
+const UP<IFloatingAlgorithm>& CAlgorithm::floatingAlgo() const {
+ return m_floating;
+}
+
+SP<ITarget> CAlgorithm::getNextCandidate(SP<ITarget> 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 <expected>
+#include <optional>
+
+namespace Layout {
+ class ITarget;
+ class IFloatingAlgorithm;
+ class ITiledAlgorithm;
+ class CSpace;
+
+ class CAlgorithm {
+ public:
+ static SP<CAlgorithm> create(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space);
+ ~CAlgorithm() = default;
+
+ void addTarget(SP<ITarget> target);
+ void moveTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt, bool reposition = false);
+ void removeTarget(SP<ITarget> target);
+
+ void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ SP<ITarget> getNextCandidate(SP<ITarget> old);
+
+ void setFloating(SP<ITarget> target, bool floating, bool reposition = false);
+
+ std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+ std::optional<Vector2D> predictSizeForNewTiledTarget();
+
+ void recalculate();
+ void recenter(SP<ITarget> t);
+
+ void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ void moveTarget(const Vector2D& Δ, SP<ITarget> target);
+
+ void updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo);
+ void updateTiledAlgo(UP<ITiledAlgorithm>&& algo);
+
+ const UP<ITiledAlgorithm>& tiledAlgo() const;
+ const UP<IFloatingAlgorithm>& floatingAlgo() const;
+
+ SP<CSpace> space() const;
+
+ size_t tiledTargets() const;
+ size_t floatingTargets() const;
+
+ private:
+ CAlgorithm(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space);
+
+ UP<ITiledAlgorithm> m_tiled;
+ UP<IFloatingAlgorithm> m_floating;
+ WP<CSpace> m_space;
+ WP<CAlgorithm> m_self;
+
+ std::vector<WP<ITarget>> 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<ITarget> 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<ITarget> target) = 0;
+
+ virtual void recenter(SP<ITarget> t);
+
+ virtual void recalculate();
+
+ protected:
+ IFloatingAlgorithm() = default;
+
+ WP<CAlgorithm> 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<void, std::string> IModeAlgorithm::layoutMsg(const std::string_view& sv) {
+ return {};
+}
+
+std::optional<Vector2D> 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 <expected>
+
+namespace Layout {
+
+ class ITarget;
+ class CAlgorithm;
+
+ class IModeAlgorithm {
+ public:
+ virtual ~IModeAlgorithm() = default;
+
+ // a completely new target
+ virtual void newTarget(SP<ITarget> target) = 0;
+
+ // a target moved into the algorithm (from another)
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt) = 0;
+
+ // a target removed
+ virtual void removeTarget(SP<ITarget> target) = 0;
+
+ // a target is being resized by a delta. Corner none likely means not interactive
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE) = 0;
+
+ // recalculate layout
+ virtual void recalculate() = 0;
+
+ // swap targets
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b) = 0;
+
+ // move a target in a given direction
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) = 0;
+
+ // optional: handle layout messages
+ virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+
+ // optional: predict new window's size
+ virtual std::optional<Vector2D> predictSizeForNewTarget();
+
+ protected:
+ IModeAlgorithm() = default;
+
+ WP<CAlgorithm> 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<ITarget> getNextCandidate(SP<ITarget> old) = 0;
+
+ protected:
+ ITiledAlgorithm() = default;
+
+ WP<CAlgorithm> 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<ITarget> 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<CWindowTarget>(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<CWindowTarget>(target); WTARGET) {
+ static auto PXWLFORCESCALEZERO = CConfigValue<Hyprlang::INT>("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<ITarget> target, std::optional<Vector2D> 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<ITarget> 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<ITarget> target) {
+ target->rememberFloatingSize(target->position().size());
+}
+
+void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> 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<ITarget> target) {
+ auto pos = target->position();
+ pos.translate(Δ);
+ target->setPositionGlobal(pos);
+
+ if (g_layoutManager->dragController()->target() == target)
+ target->warpPositionSize();
+}
+
+void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
+ auto posABackup = a->position();
+ a->setPositionGlobal(b->position());
+ b->setPositionGlobal(posABackup);
+}
+
+void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> 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<ITarget> target);
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
+ virtual void removeTarget(SP<ITarget> target);
+
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target);
+
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ private:
+ CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> 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 <hyprutils/utils/ScopeGuard.hpp>
+
+using namespace Layout;
+using namespace Layout::Tiled;
+
+struct Layout::Tiled::SDwindleNodeData {
+ WP<SDwindleNodeData> pParent;
+ bool isNode = false;
+ WP<ITarget> pTarget;
+ std::array<WP<SDwindleNodeData>, 2> children = {};
+ WP<SDwindleNodeData> 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<Hyprlang::INT>("dwindle:smart_split");
+ static auto PPRESERVESPLIT = CConfigValue<Hyprlang::INT>("dwindle:preserve_split");
+ static auto PFLMULT = CConfigValue<Hyprlang::FLOAT>("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<ITarget> target) {
+ addTarget(target);
+}
+
+void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
+ const auto WORK_AREA = m_parent->space()->workArea();
+
+ const auto PNODE = m_dwindleNodesData.emplace_back(makeShared<SDwindleNodeData>());
+ PNODE->self = PNODE;
+
+ const auto PMONITOR = m_parent->space()->workspace()->m_monitor;
+ const auto PWORKSPACE = m_parent->space()->workspace();
+
+ static auto PUSEACTIVE = CConfigValue<Hyprlang::INT>("dwindle:use_active_for_splits");
+ static auto PDEFAULTSPLIT = CConfigValue<Hyprlang::FLOAT>("dwindle:default_split_ratio");
+
+ // Populate the node with our window's data
+ PNODE->pTarget = target;
+ PNODE->isNode = false;
+
+ SP<SDwindleNodeData> 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<SDwindleNodeData>());
+
+ // 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<Hyprlang::FLOAT>("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<Hyprlang::INT>("dwindle:force_split");
+ static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue<Hyprlang::INT>("dwindle:permanent_direction_override");
+ static auto PSMARTSPLIT = CConfigValue<Hyprlang::INT>("dwindle:smart_split");
+ static auto PSPLITBIAS = CConfigValue<Hyprlang::INT>("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<ITarget> target, std::optional<Vector2D> focalPoint) {
+ m_overrideFocalPoint = focalPoint;
+ addTarget(target, false);
+ m_overrideFocalPoint.reset();
+}
+
+void CDwindleAlgorithm::removeTarget(SP<ITarget> 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<ITarget> target, eRectCorner corner) {
+ if (!validMapped(target->window()))
+ return;
+
+ const auto PNODE = getNodeFromTarget(target);
+
+ if (!PNODE)
+ return;
+
+ static auto PANIMATE = CConfigValue<Hyprlang::INT>("misc:animate_manual_resizes");
+ static auto PSMARTRESIZING = CConfigValue<Hyprlang::INT>("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<SDwindleNodeData> PVOUTER = nullptr;
+ SP<SDwindleNodeData> PVINNER = nullptr;
+ SP<SDwindleNodeData> PHOUTER = nullptr;
+ SP<SDwindleNodeData> PHINNER = nullptr;
+
+ const auto LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT || DISPLAYRIGHT;
+ const auto TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT || DISPLAYBOTTOM;
+ const auto RIGHT = corner == CORNER_TOPRIGHT || corner == CORNER_BOTTOMRIGHT || DISPLAYLEFT;
+ const auto BOTTOM = corner == CORNER_BOTTOMLEFT || corner == CORNER_BOTTOMRIGHT || DISPLAYTOP;
+ const auto 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<ITarget> CDwindleAlgorithm::getNextCandidate(SP<ITarget> 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<ITarget> a, SP<ITarget> 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<Vector2D> 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<Hyprlang::FLOAT>("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<ITarget> 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<SDwindleNodeData> CDwindleAlgorithm::getNodeFromTarget(SP<ITarget> t) {
+ for (const auto& n : m_dwindleNodesData) {
+ if (n->pTarget == t)
+ return n;
+ }
+
+ return nullptr;
+}
+
+SP<SDwindleNodeData> CDwindleAlgorithm::getNodeFromWindow(PHLWINDOW w) {
+ return w ? getNodeFromTarget(w->layoutTarget()) : nullptr;
+}
+
+int CDwindleAlgorithm::getNodes() {
+ return m_dwindleNodesData.size();
+}
+
+SP<SDwindleNodeData> CDwindleAlgorithm::getFirstNode() {
+ return m_dwindleNodesData.empty() ? nullptr : m_dwindleNodesData.at(0);
+}
+
+SP<SDwindleNodeData> CDwindleAlgorithm::getClosestNode(const Vector2D& point) {
+ SP<SDwindleNodeData> 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<SDwindleNodeData> CDwindleAlgorithm::getMasterNode() {
+ for (auto& n : m_dwindleNodesData) {
+ if (!n->pParent)
+ return n;
+ }
+ return nullptr;
+}
+
+std::expected<void, std::string> 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<SDwindleNodeData> 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<SDwindleNodeData> 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<SDwindleNodeData> 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<ITarget> target);
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
+ virtual void removeTarget(SP<ITarget> target);
+
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ virtual void recalculate();
+
+ virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
+
+ virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+ virtual std::optional<Vector2D> predictSizeForNewTarget();
+
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ private:
+ std::vector<SP<SDwindleNodeData>> m_dwindleNodesData;
+
+ struct {
+ bool started = false;
+ bool pseudo = false;
+ bool xExtent = false;
+ bool yExtent = false;
+ } m_pseudoDragFlags;
+
+ std::optional<Vector2D> m_overrideFocalPoint; // for onWindowCreatedTiling.
+
+ void addTarget(SP<ITarget> target, bool newTarget = true);
+ void calculateWorkspace();
+ SP<SDwindleNodeData> getNodeFromTarget(SP<ITarget>);
+ SP<SDwindleNodeData> getNodeFromWindow(PHLWINDOW w);
+ int getNodes();
+ SP<SDwindleNodeData> getFirstNode();
+ SP<SDwindleNodeData> getClosestNode(const Vector2D&);
+ SP<SDwindleNodeData> getMasterNode();
+
+ void toggleSplit(SP<SDwindleNodeData>);
+ void swapSplit(SP<SDwindleNodeData>);
+ void moveToRoot(SP<SDwindleNodeData>, 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 <hyprutils/utils/ScopeGuard.hpp>
+
+using namespace Layout;
+using namespace Layout::Tiled;
+
+struct Layout::Tiled::SMasterNodeData {
+ bool isMaster = false;
+ float percMaster = 0.5f;
+
+ WP<ITarget> 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<ITarget> target) {
+ addTarget(target, true);
+}
+
+void CMasterAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
+ addTarget(target, false);
+}
+
+void CMasterAlgorithm::addTarget(SP<ITarget> target, bool firstMap) {
+ static auto PNEWONACTIVE = CConfigValue<std::string>("master:new_on_active");
+ static auto PNEWONTOP = CConfigValue<Hyprlang::INT>("master:new_on_top");
+ static auto PNEWSTATUS = CConfigValue<std::string>("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<SMasterNodeData> {
+ 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<SMasterNodeData>());
+ }
+ }
+ return *PNEWONTOP ? *m_masterNodesData.emplace(m_masterNodesData.begin(), makeShared<SMasterNodeData>()) : m_masterNodesData.emplace_back(makeShared<SMasterNodeData>());
+ }();
+
+ PNODE->pTarget = target;
+
+ const auto WINDOWSONWORKSPACE = getNodesNo();
+ static auto PMFACT = CConfigValue<Hyprlang::FLOAT>("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<Hyprlang::INT>("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::size_t>(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<std::ptrdiff_t>(srcIndex));
+ v.insert(v.begin() + static_cast<std::ptrdiff_t>(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<ITarget> target) {
+ const auto MASTERSLEFT = getMastersNo();
+ static auto SMALLSPLIT = CConfigValue<Hyprlang::INT>("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<ITarget> target, eRectCorner corner) {
+ const auto PNODE = getNodeFromTarget(target);
+
+ if (!PNODE)
+ return;
+
+ const auto PMONITOR = m_parent->space()->workspace()->m_monitor;
+ static auto SLAVECOUNTFORCENTER = CConfigValue<Hyprlang::INT>("master:slave_count_for_center_master");
+ static auto PSMARTRESIZING = CConfigValue<Hyprlang::INT>("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<ITarget> a, SP<ITarget> b) {
+ auto nodeA = getNodeFromTarget(a);
+ auto nodeB = getNodeFromTarget(b);
+
+ if (nodeA)
+ nodeA->pTarget = b;
+ if (nodeB)
+ nodeB->pTarget = a;
+}
+
+void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> 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<void, std::string> CMasterAlgorithm::layoutMsg(const std::string_view& sv) {
+ auto switchToWindow = [&](SP<ITarget> 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 <master | child | auto> <ignoremaster>
+ // 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 <master | previous | auto>
+ // 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<Hyprlang::INT>("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<Vector2D> CMasterAlgorithm::predictSizeForNewTarget() {
+ static auto PNEWSTATUS = CConfigValue<std::string>("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<eOrientation>& 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<eOrientation>& cycle) {
+ for (int i = 0; i <= ORIENTATION_CENTER; ++i) {
+ cycle.push_back(sc<eOrientation>(i));
+ }
+}
+
+void CMasterAlgorithm::runOrientationCycle(Hyprutils::String::CVarList2* vars, int next) {
+ std::vector<eOrientation> 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<int>(cycle.size()))
+ nextOrPrev = nextOrPrev % sc<int>(cycle.size());
+ else if (nextOrPrev < 0)
+ nextOrPrev = cycle.size() + (nextOrPrev % sc<int>(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<SMasterNodeData> CMasterAlgorithm::getNodeFromWindow(PHLWINDOW x) {
+ return x ? getNodeFromTarget(x->layoutTarget()) : nullptr;
+}
+
+SP<SMasterNodeData> CMasterAlgorithm::getNodeFromTarget(SP<ITarget> x) {
+ for (const auto& n : m_masterNodesData) {
+ if (n->pTarget == x)
+ return n;
+ }
+
+ return nullptr;
+}
+
+SP<SMasterNodeData> 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<Hyprlang::INT>("master:slave_count_for_center_master");
+ static auto CMFALLBACK = CConfigValue<std::string>("master:center_master_fallback");
+ static auto PIGNORERESERVED = CConfigValue<Hyprlang::INT>("master:center_ignores_reserved");
+ static auto PSMARTRESIZING = CConfigValue<Hyprlang::INT>("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<Hyprlang::INT>("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<ITarget> CMasterAlgorithm::getNextCandidate(SP<ITarget> 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<ITarget> CMasterAlgorithm::getNextTarget(SP<ITarget> 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<SMasterNodeData> CMasterAlgorithm::getClosestNode(const Vector2D& point) {
+ SP<SMasterNodeData> 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 <hyprutils/string/VarList2.hpp>
+
+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<ITarget> 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<ITarget> target);
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
+ virtual void removeTarget(SP<ITarget> target);
+
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ virtual void recalculate();
+
+ virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
+
+ virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+ virtual std::optional<Vector2D> predictSizeForNewTarget();
+
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ private:
+ std::vector<SP<SMasterNodeData>> m_masterNodesData;
+ SMasterWorkspaceData m_workspaceData;
+
+ void addTarget(SP<ITarget> target, bool firstMap);
+
+ bool m_forceWarps = false;
+
+ void buildOrientationCycleVectorFromVars(std::vector<eOrientation>& cycle, Hyprutils::String::CVarList2* vars);
+ void buildOrientationCycleVectorFromEOperation(std::vector<eOrientation>& cycle);
+ void runOrientationCycle(Hyprutils::String::CVarList2* vars, int next);
+ eOrientation getDynamicOrientation();
+ int getNodesNo();
+ SP<SMasterNodeData> getNodeFromWindow(PHLWINDOW);
+ SP<SMasterNodeData> getNodeFromTarget(SP<ITarget>);
+ SP<SMasterNodeData> getMasterNode();
+ SP<SMasterNodeData> getClosestNode(const Vector2D&);
+ void calculateWorkspace();
+ SP<ITarget> getNextTarget(SP<ITarget>, 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 <hyprutils/string/VarList2.hpp>
+#include <hyprutils/string/ConstVarList.hpp>
+#include <hyprutils/utils/ScopeGuard.hpp>
+
+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<Desktop::View::SWindowActiveEvent>(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<SMonocleTargetData> CMonocleAlgorithm::dataFor(SP<ITarget> t) {
+ for (auto& data : m_targetDatas) {
+ if (data->target.lock() == t)
+ return data;
+ }
+ return nullptr;
+}
+
+void CMonocleAlgorithm::newTarget(SP<ITarget> target) {
+ const auto DATA = m_targetDatas.emplace_back(makeShared<SMonocleTargetData>(target));
+
+ m_currentVisibleIndex = m_targetDatas.size() - 1;
+
+ recalculate();
+}
+
+void CMonocleAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
+ newTarget(target);
+}
+
+void CMonocleAlgorithm::removeTarget(SP<ITarget> 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<ITarget> 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<ITarget> CMonocleAlgorithm::getNextCandidate(SP<ITarget> 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<void, std::string> 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<Vector2D> CMonocleAlgorithm::predictSizeForNewTarget() {
+ const auto WORK_AREA = m_parent->space()->workArea();
+ return WORK_AREA.size();
+}
+
+void CMonocleAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
+ auto nodeA = dataFor(a);
+ auto nodeB = dataFor(b);
+
+ if (nodeA)
+ nodeA->target = b;
+ if (nodeB)
+ nodeB->target = a;
+
+ recalculate();
+}
+
+void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> 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<ITarget> 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<ITarget> 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 <vector>
+
+namespace Layout::Tiled {
+
+ struct SMonocleTargetData {
+ SMonocleTargetData(SP<ITarget> t) : target(t) {
+ ;
+ }
+
+ WP<ITarget> target;
+ CBox layoutBox;
+ };
+
+ class CMonocleAlgorithm : public ITiledAlgorithm {
+ public:
+ CMonocleAlgorithm();
+ virtual ~CMonocleAlgorithm();
+
+ virtual void newTarget(SP<ITarget> target);
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
+ virtual void removeTarget(SP<ITarget> target);
+
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ virtual void recalculate();
+
+ virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
+
+ virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+ virtual std::optional<Vector2D> predictSizeForNewTarget();
+
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ private:
+ std::vector<SP<SMonocleTargetData>> m_targetDatas;
+ SP<HOOK_CALLBACK_FN> m_focusCallback;
+
+ int m_currentVisibleIndex = 0;
+
+ SP<SMonocleTargetData> dataFor(SP<ITarget> t);
+ void cycleNext();
+ void cyclePrev();
+ void focusTargetUpdate(SP<ITarget> target);
+ void updateVisible();
+ SP<ITarget> 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 <algorithm>
+#include <cmath>
+
+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 <vector>
+
+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<float> targetSizes; // sizes along secondary axis for each target in this strip
+ WP<SColumnData> 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<SStripData> 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 <hyprutils/string/VarList2.hpp>
+#include <hyprutils/string/ConstVarList.hpp>
+#include <hyprutils/utils/ScopeGuard.hpp>
+
+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<SScrollingTargetData> 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<SScrollingTargetData> target, float size) {
+ for (size_t i = 0; i < targetDatas.size(); ++i) {
+ if (targetDatas[i] == target) {
+ setTargetSize(i, size);
+ return;
+ }
+ }
+}
+
+void SColumnData::add(SP<ITarget> 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<SScrollingTargetData>(t, self.lock()));
+ setTargetSize(targetDatas.size() - 1, newSize);
+}
+
+void SColumnData::add(SP<ITarget> 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<SScrollingTargetData>(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<SScrollingTargetData> 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<SScrollingTargetData> 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<ITarget> 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<ITarget> 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<SScrollingTargetData> 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<SScrollingTargetData> 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<SScrollingTargetData> SColumnData::next(SP<SScrollingTargetData> w) {
+ for (size_t i = 0; i < targetDatas.size() - 1; ++i) {
+ if (targetDatas[i] != w)
+ continue;
+
+ return targetDatas[i + 1];
+ }
+
+ return nullptr;
+}
+
+SP<SScrollingTargetData> SColumnData::prev(SP<SScrollingTargetData> 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<ITarget> 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<CScrollTapeController>(SCROLL_DIR_RIGHT);
+}
+
+SP<SColumnData> SScrollingData::add() {
+ static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
+ auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
+ col->self = col;
+
+ size_t stripIdx = controller->addStrip(*PCOLWIDTH);
+ controller->getStrip(stripIdx).userData = col;
+
+ return col;
+}
+
+SP<SColumnData> SScrollingData::add(int after) {
+ static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
+ auto col = makeShared<SColumnData>(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<SColumnData> c) {
+ for (size_t i = 0; i < columns.size(); ++i) {
+ if (columns[i] == c)
+ return i;
+ }
+
+ return -1;
+}
+
+void SScrollingData::remove(SP<SColumnData> c) {
+ // find index before removing
+ int64_t index = idx(c);
+
+ std::erase(columns, c);
+
+ // sync with controller
+ if (index >= 0)
+ controller->removeStrip(index);
+}
+
+SP<SColumnData> SScrollingData::next(SP<SColumnData> 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<SColumnData> SScrollingData::prev(SP<SColumnData> 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<SColumnData> c) {
+ if (!c)
+ return;
+
+ static const auto PFSONONE = CConfigValue<Hyprlang::INT>("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<SColumnData> c) {
+ if (!c)
+ return;
+
+ static const auto PFSONONE = CConfigValue<Hyprlang::INT>("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<SColumnData> c) {
+ if (!c)
+ return;
+
+ static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("scrolling:focus_fit_method");
+
+ if (*PFITMETHOD == 1)
+ fitCol(c);
+ else
+ centerCol(c);
+}
+
+SP<SColumnData> SScrollingData::atCenter() {
+ static const auto PFSONONE = CConfigValue<Hyprlang::INT>("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<Hyprlang::INT>("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<Hyprlang::INT>("scrolling:fullscreen_on_one_column");
+ const auto USABLE = algorithm->usableArea();
+
+ return controller->calculateMaxExtent(USABLE, *PFSONONE);
+}
+
+bool SScrollingData::visible(SP<SColumnData> c) {
+ static const auto PFSONONE = CConfigValue<Hyprlang::INT>("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<Hyprlang::STRING>("scrolling:explicit_column_widths");
+ static const auto PCONFDIRECTION = CConfigValue<Hyprlang::STRING>("scrolling:direction");
+
+ m_scrollingData = makeShared<SScrollingData>(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<Hyprlang::STRING>("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<IPointer::SButtonEvent>(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<Desktop::View::SWindowActiveEvent>(param);
+ const auto PWINDOW = E.window;
+
+ if (!PWINDOW)
+ return;
+
+ static const auto PFOLLOW_FOCUS = CConfigValue<Hyprlang::INT>("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<ITarget> target, bool hardInput) {
+ static const auto PFOLLOW_FOCUS_MIN_PERC = CConfigValue<Hyprlang::FLOAT>("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<Hyprlang::INT>("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<ITarget> 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<SScrollingTargetData> droppingData = droppingOn ? dataFor(droppingOn->layoutTarget()) : nullptr;
+ SP<SColumnData> 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<ITarget> target, std::optional<Vector2D> focalPoint) {
+ newTarget(target);
+}
+
+void CScrollingAlgorithm::removeTarget(SP<ITarget> 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<ITarget> 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<Hyprlang::INT>("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<SScrollingTargetData> CScrollingAlgorithm::closestNode(const Vector2D& posGlobglobgabgalab) {
+ SP<SScrollingTargetData> 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<ITarget> CScrollingAlgorithm::getNextCandidate(SP<ITarget> old) {
+ const auto CENTER = old->position().middle();
+
+ const auto NODE = closestNode(CENTER);
+
+ if (!NODE)
+ return nullptr;
+
+ return NODE->target.lock();
+}
+
+void CScrollingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> 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<ITarget> t, Math::eDirection dir, bool silent) {
+ moveTargetTo(t, dir, silent);
+}
+
+void CScrollingAlgorithm::moveTargetTo(SP<ITarget> 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<void, std::string> CScrollingAlgorithm::layoutMsg(const std::string_view& sv) {
+ auto centerOrFit = [this](const SP<SColumnData> COL) -> void {
+ static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("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<SP<SColumnData>> 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<Hyprlang::INT>("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<Vector2D> CScrollingAlgorithm::predictSizeForNewTarget() {
+ return std::nullopt;
+}
+
+void CScrollingAlgorithm::focusTargetUpdate(SP<ITarget> 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<SScrollingTargetData> CScrollingAlgorithm::findBestNeighbor(SP<SScrollingTargetData> pCurrent, SP<SColumnData> 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<SP<SScrollingTargetData>> 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<SScrollingTargetData>& t) { return t->layoutBox.y; });
+ return *topmost;
+ }
+ if (!pTargetCol->targetDatas.empty())
+ return pTargetCol->targetDatas.front();
+ return nullptr;
+}
+
+SP<SScrollingTargetData> CScrollingAlgorithm::dataFor(SP<ITarget> 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<Hyprlang::STRING>("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 <vector>
+
+namespace Layout::Tiled {
+ class CScrollingAlgorithm;
+ struct SColumnData;
+ struct SScrollingData;
+
+ struct SScrollingTargetData {
+ SScrollingTargetData(SP<ITarget> t, SP<SColumnData> col) : target(t), column(col) {
+ ;
+ }
+
+ WP<ITarget> target;
+ WP<SColumnData> column;
+ bool ignoreFullscreenChecks = false;
+
+ CBox layoutBox;
+ };
+
+ struct SColumnData {
+ SColumnData(SP<SScrollingData> data) : scrollingData(data) {
+ ;
+ }
+
+ void add(SP<ITarget> t);
+ void add(SP<ITarget> t, int after);
+ void add(SP<SScrollingTargetData> w);
+ void add(SP<SScrollingTargetData> w, int after);
+ void remove(SP<ITarget> t);
+ bool has(SP<ITarget> t);
+ size_t idx(SP<ITarget> t);
+
+ // index of lowest target that is above y.
+ size_t idxForHeight(float y);
+
+ void up(SP<SScrollingTargetData> w);
+ void down(SP<SScrollingTargetData> w);
+
+ SP<SScrollingTargetData> next(SP<SScrollingTargetData> w);
+ SP<SScrollingTargetData> prev(SP<SScrollingTargetData> w);
+
+ std::vector<SP<SScrollingTargetData>> targetDatas;
+ WP<SScrollingData> scrollingData;
+ WP<SScrollingTargetData> lastFocusedTarget;
+
+ WP<SColumnData> 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<SScrollingTargetData> target) const;
+ void setTargetSize(SP<SScrollingTargetData> target, float size);
+ };
+
+ struct SScrollingData {
+ SScrollingData(CScrollingAlgorithm* algo);
+
+ std::vector<SP<SColumnData>> columns;
+
+ UP<CScrollTapeController> controller;
+
+ SP<SColumnData> add();
+ SP<SColumnData> add(int after);
+ int64_t idx(SP<SColumnData> c);
+ void remove(SP<SColumnData> c);
+ double maxWidth();
+ SP<SColumnData> next(SP<SColumnData> c);
+ SP<SColumnData> prev(SP<SColumnData> c);
+ SP<SColumnData> atCenter();
+
+ bool visible(SP<SColumnData> c);
+ void centerCol(SP<SColumnData> c);
+ void fitCol(SP<SColumnData> c);
+ void centerOrFitCol(SP<SColumnData> c);
+
+ void recalculate(bool forceInstant = false);
+
+ CScrollingAlgorithm* algorithm = nullptr;
+ WP<SScrollingData> self;
+ std::optional<double> lockedCameraOffset;
+ };
+
+ class CScrollingAlgorithm : public ITiledAlgorithm {
+ public:
+ CScrollingAlgorithm();
+ virtual ~CScrollingAlgorithm();
+
+ virtual void newTarget(SP<ITarget> target);
+ virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
+ virtual void removeTarget(SP<ITarget> target);
+
+ virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
+ virtual void recalculate();
+
+ virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
+
+ virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
+ virtual std::optional<Vector2D> predictSizeForNewTarget();
+
+ virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
+ virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
+
+ CBox usableArea();
+
+ private:
+ SP<SScrollingData> m_scrollingData;
+
+ SP<HOOK_CALLBACK_FN> m_configCallback;
+ SP<HOOK_CALLBACK_FN> m_focusCallback;
+ SP<HOOK_CALLBACK_FN> m_mouseButtonCallback;
+
+ struct {
+ std::vector<float> configuredWidths;
+ } m_config;
+
+ eScrollDirection getDynamicDirection();
+
+ SP<SScrollingTargetData> findBestNeighbor(SP<SScrollingTargetData> pCurrent, SP<SColumnData> pTargetCol);
+ SP<SScrollingTargetData> dataFor(SP<ITarget> t);
+ SP<SScrollingTargetData> closestNode(const Vector2D& posGlobglobgabgalab);
+
+ void focusTargetUpdate(SP<ITarget> target);
+ void moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent);
+ void focusOnInput(SP<ITarget> target, bool hardInput);
+
+ friend struct SScrollingData;
+ };
+};