2026-02-21 21:30:39 +00:00
|
|
|
#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"
|
2026-02-22 23:30:10 +00:00
|
|
|
#include "../../../../event/EventBus.hpp"
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
#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) {
|
2026-03-02 00:04:02 +05:45
|
|
|
if (targetDatas.empty())
|
|
|
|
|
return 0;
|
2026-02-21 21:30:39 +00:00
|
|
|
for (size_t i = 0; i < targetDatas.size(); ++i) {
|
|
|
|
|
if (targetDatas[i]->target->position().y < y)
|
|
|
|
|
continue;
|
2026-03-02 00:04:02 +05:45
|
|
|
return i == 0 ? 0 : i - 1;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
bool SColumnData::up(SP<SScrollingTargetData> w) {
|
2026-02-21 21:30:39 +00:00
|
|
|
for (size_t i = 1; i < targetDatas.size(); ++i) {
|
|
|
|
|
if (targetDatas[i] != w)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
std::swap(targetDatas[i], targetDatas[i - 1]);
|
2026-03-02 19:31:33 +00:00
|
|
|
return true;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
2026-03-02 19:31:33 +00:00
|
|
|
|
|
|
|
|
return false;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
bool SColumnData::down(SP<SScrollingTargetData> w) {
|
2026-02-21 21:30:39 +00:00
|
|
|
for (size_t i = 0; i < targetDatas.size() - 1; ++i) {
|
|
|
|
|
if (targetDatas[i] != w)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
std::swap(targetDatas[i], targetDatas[i + 1]);
|
2026-03-02 19:31:33 +00:00
|
|
|
return true;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
2026-03-02 19:31:33 +00:00
|
|
|
|
|
|
|
|
return false;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-03-01 10:15:22 +00:00
|
|
|
auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
|
|
|
|
|
col->self = col;
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-01 10:15:22 +00:00
|
|
|
size_t stripIdx = controller->addStrip(algorithm->defaultColumnWidth());
|
2026-02-21 21:30:39 +00:00
|
|
|
controller->getStrip(stripIdx).userData = col;
|
|
|
|
|
|
|
|
|
|
return col;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SP<SColumnData> SScrollingData::add(int after) {
|
2026-03-01 10:15:22 +00:00
|
|
|
auto col = makeShared<SColumnData>(self.lock());
|
|
|
|
|
col->self = col;
|
2026-02-21 21:30:39 +00:00
|
|
|
columns.insert(columns.begin() + after + 1, col);
|
|
|
|
|
|
2026-03-01 10:15:22 +00:00
|
|
|
controller->insertStrip(after, algorithm->defaultColumnWidth());
|
2026-02-21 21:30:39 +00:00
|
|
|
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) {
|
2026-02-24 11:33:21 +00:00
|
|
|
if (!algorithm->m_parent->space()->workspace() || algorithm->m_parent->space()->workspace()->m_hasFullscreenWindow)
|
2026-02-21 21:30:39 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 12:27:00 +00:00
|
|
|
bool SScrollingData::visible(SP<SColumnData> c, bool full) {
|
2026-02-21 21:30:39 +00:00
|
|
|
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)
|
2026-02-24 12:27:00 +00:00
|
|
|
return controller->isStripVisible(colIdx, USABLE, *PFSONONE, full);
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-02 07:51:56 -05:00
|
|
|
// Helper to parse explicit_column_widths string
|
|
|
|
|
auto parseColumnWidths = [](const std::string& dir) -> std::vector<float> {
|
|
|
|
|
auto widthVec = std::vector<float>();
|
|
|
|
|
|
|
|
|
|
CConstVarList widths(dir, 0, ',');
|
|
|
|
|
for (auto& w : widths) {
|
|
|
|
|
try {
|
|
|
|
|
widthVec.emplace_back(std::clamp(std::stof(std::string{w}), MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH));
|
|
|
|
|
} catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); }
|
|
|
|
|
}
|
|
|
|
|
if (widthVec.empty())
|
|
|
|
|
widthVec = {0.333, 0.5, 0.667, 1.0}; // default
|
|
|
|
|
return widthVec;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-21 21:30:39 +00:00
|
|
|
// 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
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 07:51:56 -05:00
|
|
|
m_configCallback = Event::bus()->m_events.config.reloaded.listen([this, parseColumnWidths, parseDirection] {
|
2026-02-21 21:30:39 +00:00
|
|
|
static const auto PCONFDIRECTION = CConfigValue<Hyprlang::STRING>("scrolling:direction");
|
|
|
|
|
|
|
|
|
|
m_config.configuredWidths.clear();
|
2026-03-02 07:51:56 -05:00
|
|
|
m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS);
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
// Update scroll direction
|
|
|
|
|
m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION));
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:30:10 +00:00
|
|
|
m_mouseButtonCallback = Event::bus()->m_events.input.mouse.button.listen([this](IPointer::SButtonEvent e, Event::SCallbackInfo&) {
|
2026-02-24 12:27:00 +00:00
|
|
|
static const auto PFOLLOW_FOCUS = CConfigValue<Hyprlang::INT>("scrolling:follow_focus");
|
|
|
|
|
|
|
|
|
|
if (*PFOLLOW_FOCUS && e.state == WL_POINTER_BUTTON_STATE_RELEASED && Desktop::focusState()->window())
|
|
|
|
|
focusOnInput(Desktop::focusState()->window()->layoutTarget(), INPUT_MODE_CLICK);
|
2026-02-21 21:30:39 +00:00
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:30:10 +00:00
|
|
|
m_focusCallback = Event::bus()->m_events.window.active.listen([this](PHLWINDOW pWindow, Desktop::eFocusReason reason) {
|
|
|
|
|
if (!pWindow)
|
2026-02-21 21:30:39 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
static const auto PFOLLOW_FOCUS = CConfigValue<Hyprlang::INT>("scrolling:follow_focus");
|
|
|
|
|
|
2026-02-22 23:30:10 +00:00
|
|
|
if (!*PFOLLOW_FOCUS && !Desktop::isHardInputFocusReason(reason))
|
2026-02-21 21:30:39 +00:00
|
|
|
return;
|
|
|
|
|
|
2026-02-22 23:30:10 +00:00
|
|
|
if (pWindow->m_workspace != m_parent->space()->workspace())
|
2026-02-21 21:30:39 +00:00
|
|
|
return;
|
|
|
|
|
|
2026-02-22 23:30:10 +00:00
|
|
|
const auto TARGET = pWindow->layoutTarget();
|
2026-02-21 21:30:39 +00:00
|
|
|
if (!TARGET || TARGET->floating())
|
|
|
|
|
return;
|
|
|
|
|
|
2026-02-24 12:27:00 +00:00
|
|
|
focusOnInput(TARGET, reason == Desktop::FOCUS_REASON_CLICK ? INPUT_MODE_CLICK : (Desktop::isHardInputFocusReason(reason) ? INPUT_MODE_KB : INPUT_MODE_SOFT));
|
2026-02-21 21:30:39 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Initialize default widths and direction
|
2026-03-02 07:51:56 -05:00
|
|
|
m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS);
|
2026-02-21 21:30:39 +00:00
|
|
|
m_scrollingData->controller->setDirection(parseDirection(*PCONFDIRECTION));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CScrollingAlgorithm::~CScrollingAlgorithm() {
|
|
|
|
|
m_configCallback.reset();
|
|
|
|
|
m_focusCallback.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 12:27:00 +00:00
|
|
|
void CScrollingAlgorithm::focusOnInput(SP<ITarget> target, eInputMode input) {
|
2026-02-21 21:30:39 +00:00
|
|
|
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;
|
|
|
|
|
|
2026-02-24 12:27:00 +00:00
|
|
|
if (*PFOLLOW_FOCUS_MIN_PERC > 0.F && input == INPUT_MODE_SOFT) {
|
2026-02-21 21:30:39 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 12:27:00 +00:00
|
|
|
// if we moved via non-kb, and it's fully visible, ignore
|
|
|
|
|
if (m_scrollingData->visible(TARGETDATA->column.lock(), true) && input != INPUT_MODE_KB)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-02-21 21:30:39 +00:00
|
|
|
static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("scrolling:focus_fit_method");
|
2026-02-24 12:27:00 +00:00
|
|
|
if (*PFITMETHOD == 1 || input == INPUT_MODE_CLICK)
|
2026-02-21 21:30:39 +00:00
|
|
|
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
|
2026-03-02 21:23:24 +00:00
|
|
|
const auto USABLE = usableArea();
|
|
|
|
|
const bool isPrimaryHoriz = m_scrollingData->controller->isPrimaryHorizontal();
|
|
|
|
|
const double usablePrimary = isPrimaryHoriz ? USABLE.w : USABLE.h;
|
|
|
|
|
m_scrollingData->controller->adjustOffset(-(usablePrimary * DATA->column->getColumnWidth()));
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DATA->column->remove(target);
|
|
|
|
|
|
|
|
|
|
if (!DATA->column) {
|
|
|
|
|
// column got removed, let's ensure we don't leave any cringe extra space
|
2026-03-02 21:23:24 +00:00
|
|
|
const auto USABLE = usableArea();
|
|
|
|
|
const bool isPrimaryHoriz = m_scrollingData->controller->isPrimaryHorizontal();
|
|
|
|
|
const double usablePrimary = isPrimaryHoriz ? USABLE.w : USABLE.h;
|
|
|
|
|
const double newOffset = std::clamp(m_scrollingData->controller->getOffset(), 0.0, std::max(m_scrollingData->maxWidth() - usablePrimary, 1.0));
|
2026-02-21 21:30:39 +00:00
|
|
|
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() {
|
2026-02-24 12:27:00 +00:00
|
|
|
if (Desktop::focusState()->window()) {
|
|
|
|
|
const auto TARGET = Desktop::focusState()->window()->layoutTarget();
|
|
|
|
|
|
|
|
|
|
const auto TARGETDATA = dataFor(TARGET);
|
|
|
|
|
|
|
|
|
|
if (TARGETDATA && !m_scrollingData->visible(TARGETDATA->column.lock(), true))
|
|
|
|
|
focusOnInput(Desktop::focusState()->window()->layoutTarget(), INPUT_MODE_KB);
|
|
|
|
|
}
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
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) {
|
2026-03-02 21:39:06 +00:00
|
|
|
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
|
|
|
|
|
|
|
|
|
|
const auto DATA = dataFor(t);
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
if (!DATA)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const auto CURRENT_COL = DATA->column.lock();
|
|
|
|
|
const auto current_idx = m_scrollingData->idx(CURRENT_COL);
|
|
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
auto rotateDir = [this](Math::eDirection dir) -> Math::eDirection {
|
|
|
|
|
switch (m_scrollingData->controller->getDirection()) {
|
|
|
|
|
case SCROLL_DIR_RIGHT: return dir;
|
|
|
|
|
case SCROLL_DIR_LEFT: {
|
|
|
|
|
if (dir == Math::DIRECTION_LEFT)
|
|
|
|
|
return Math::DIRECTION_RIGHT;
|
|
|
|
|
if (dir == Math::DIRECTION_RIGHT)
|
|
|
|
|
return Math::DIRECTION_LEFT;
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
case SCROLL_DIR_UP: {
|
|
|
|
|
switch (dir) {
|
|
|
|
|
case Math::DIRECTION_UP: return Math::DIRECTION_RIGHT;
|
|
|
|
|
case Math::DIRECTION_DOWN: return Math::DIRECTION_LEFT;
|
|
|
|
|
case Math::DIRECTION_LEFT: return Math::DIRECTION_DOWN;
|
|
|
|
|
case Math::DIRECTION_RIGHT: return Math::DIRECTION_UP;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
case SCROLL_DIR_DOWN: {
|
|
|
|
|
switch (dir) {
|
|
|
|
|
case Math::DIRECTION_UP: return Math::DIRECTION_LEFT;
|
|
|
|
|
case Math::DIRECTION_DOWN: return Math::DIRECTION_RIGHT;
|
|
|
|
|
case Math::DIRECTION_LEFT: return Math::DIRECTION_DOWN;
|
|
|
|
|
case Math::DIRECTION_RIGHT: return Math::DIRECTION_UP;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
default: break;
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
return dir;
|
|
|
|
|
};
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
const auto ROTATED_DIR = rotateDir(dir);
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
auto commenceDir = [&]() -> bool {
|
|
|
|
|
if (ROTATED_DIR == Math::DIRECTION_LEFT) {
|
|
|
|
|
const auto COL = m_scrollingData->prev(DATA->column.lock());
|
|
|
|
|
|
|
|
|
|
// ignore moves to the origin if we are alone
|
|
|
|
|
if (!COL && current_idx == 0 && DATA->column->targetDatas.size() == 1)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} else if (ROTATED_DIR == Math::DIRECTION_RIGHT) {
|
|
|
|
|
const auto COL = m_scrollingData->next(DATA->column.lock());
|
|
|
|
|
|
|
|
|
|
// ignore move to the right when there is no next column and we're alone
|
|
|
|
|
if (!COL && current_idx == (int64_t)m_scrollingData->columns.size() - 1 && DATA->column->targetDatas.size() == 1)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} else if (ROTATED_DIR == Math::DIRECTION_UP)
|
|
|
|
|
return DATA->column->up(DATA);
|
|
|
|
|
else if (ROTATED_DIR == Math::DIRECTION_DOWN)
|
|
|
|
|
return DATA->column->down(DATA);
|
2026-02-21 21:30:39 +00:00
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!commenceDir()) {
|
|
|
|
|
// dir wasn't commenced, move to a workspace if possible
|
|
|
|
|
// with the original dir
|
2026-03-02 21:39:06 +00:00
|
|
|
|
|
|
|
|
if (!*PMONITORFALLBACK)
|
|
|
|
|
return; // noop
|
|
|
|
|
|
2026-03-02 19:31:33 +00:00
|
|
|
const auto MONINDIR = g_pCompositor->getMonitorInDirection(m_parent->space()->workspace()->m_monitor.lock(), dir);
|
|
|
|
|
if (MONINDIR && MONINDIR != m_parent->space()->workspace()->m_monitor && MONINDIR->m_activeWorkspace) {
|
2026-03-02 21:10:21 +00:00
|
|
|
t->assignToSpace(MONINDIR->m_activeWorkspace->m_space, focalPointForDir(t, dir));
|
2026-03-02 19:31:33 +00:00
|
|
|
|
|
|
|
|
m_scrollingData->recalculate();
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
m_scrollingData->recalculate();
|
|
|
|
|
focusTargetUpdate(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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") {
|
2026-03-05 09:08:40 -05:00
|
|
|
const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr);
|
|
|
|
|
static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");
|
|
|
|
|
static const auto PCONFWRAPFOCUS = CConfigValue<Hyprlang::INT>("scrolling:wrap_focus");
|
2026-02-21 21:30:39 +00:00
|
|
|
|
|
|
|
|
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
|
2026-03-05 09:08:40 -05:00
|
|
|
PREV = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.back() : m_scrollingData->columns.front();
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-05 09:08:40 -05:00
|
|
|
NEXT = (*PCONFWRAPFOCUS == 1) ? m_scrollingData->columns.front() : m_scrollingData->columns.back();
|
2026-02-21 21:30:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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") {
|
2026-03-05 09:08:40 -05:00
|
|
|
static const auto PCONFWRAPSWAPCOL = CConfigValue<Hyprlang::INT>("scrolling:wrap_swapcol");
|
|
|
|
|
|
2026-02-21 21:30:39 +00:00
|
|
|
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")
|
2026-03-05 09:08:40 -05:00
|
|
|
if (*PCONFWRAPSWAPCOL == 1)
|
|
|
|
|
targetIdx = (currentIdx == 0) ? (colCount - 1) : (currentIdx - 1);
|
|
|
|
|
else
|
|
|
|
|
targetIdx = (currentIdx == 0) ? 0 : (currentIdx - 1);
|
2026-02-21 21:30:39 +00:00
|
|
|
else if (direction == "r")
|
2026-03-05 09:08:40 -05:00
|
|
|
if (*PCONFWRAPSWAPCOL == 1)
|
|
|
|
|
targetIdx = (currentIdx == (int64_t)colCount - 1) ? 0 : (currentIdx + 1);
|
|
|
|
|
else
|
|
|
|
|
targetIdx = (currentIdx == (int64_t)colCount - 1) ? (colCount - 1) : (currentIdx + 1);
|
2026-02-21 21:30:39 +00:00
|
|
|
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();
|
2026-02-24 11:33:21 +00:00
|
|
|
|
|
|
|
|
// doesn't matter, this happens when this algo is about to be destroyed
|
2026-03-03 20:59:18 +00:00
|
|
|
if (!m_parent->space()->workspace() || !m_parent->space()->workspace()->m_monitor)
|
2026-02-24 11:33:21 +00:00
|
|
|
return box;
|
|
|
|
|
|
2026-02-21 21:30:39 +00:00
|
|
|
box.translate(-m_parent->space()->workspace()->m_monitor->m_position);
|
|
|
|
|
return box;
|
|
|
|
|
}
|
2026-03-01 10:15:22 +00:00
|
|
|
|
|
|
|
|
float CScrollingAlgorithm::defaultColumnWidth() {
|
|
|
|
|
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("scrolling:column_width");
|
|
|
|
|
return std::clamp(*PCOLWIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
|
|
|
|
|
}
|