diff options
| author | Vaxry <43317083+vaxerski@users.noreply.github.com> | 2026-02-21 21:30:39 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-21 21:30:39 +0000 |
| commit | 723870337f299d4c80aa0048144f3a1ca1c885bd (patch) | |
| tree | ca3279ae5dbc841f676c64d2f58f895de13cd3bf /src/layout/algorithm/tiled/scrolling | |
| parent | 51f8849e541f0a8150ac3a43fa928c1e299daf18 (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/tiled/scrolling')
4 files changed, 1925 insertions, 0 deletions
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; + }; +}; |
