hyprland-plugins/hyprscrolling/Scrolling.cpp

1543 lines
52 KiB
C++
Raw Normal View History

2025-05-02 19:48:37 +01:00
#include "Scrolling.hpp"
2025-05-22 15:05:40 +02:00
#include <algorithm>
2025-05-02 19:48:37 +01:00
#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/desktop/state/FocusState.hpp>
#include <hyprland/src/managers/input/InputManager.hpp>
#include <hyprland/src/managers/eventLoop/EventLoopManager.hpp>
2025-05-02 19:48:37 +01:00
#include <hyprland/src/config/ConfigManager.hpp>
#include <hyprland/src/config/ConfigValue.hpp>
#include <hyprland/src/render/Renderer.hpp>
#include <hyprutils/string/ConstVarList.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::Utils;
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;
//
2025-05-02 19:48:37 +01:00
void SColumnData::add(PHLWINDOW w) {
for (auto& wd : windowDatas) {
wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
}
windowDatas.emplace_back(makeShared<SScrollingWindowData>(w, self.lock(), 1.F / (float)(windowDatas.size() + 1)));
2025-05-02 19:48:37 +01:00
}
void SColumnData::add(PHLWINDOW w, int after) {
for (auto& wd : windowDatas) {
wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
}
windowDatas.insert(windowDatas.begin() + after + 1, makeShared<SScrollingWindowData>(w, self.lock(), 1.F / (float)(windowDatas.size() + 1)));
}
2025-05-02 19:48:37 +01:00
void SColumnData::add(SP<SScrollingWindowData> w) {
for (auto& wd : windowDatas) {
wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
}
2025-05-02 19:48:37 +01:00
windowDatas.emplace_back(w);
w->column = self;
w->windowSize = 1.F / (float)(windowDatas.size());
2025-05-02 19:48:37 +01:00
}
void SColumnData::add(SP<SScrollingWindowData> w, int after) {
for (auto& wd : windowDatas) {
wd->windowSize *= (float)windowDatas.size() / (float)(windowDatas.size() + 1);
}
windowDatas.insert(windowDatas.begin() + after + 1, w);
w->column = self;
w->windowSize = 1.F / (float)(windowDatas.size());
}
size_t SColumnData::idx(PHLWINDOW w) {
for (size_t i = 0; i < windowDatas.size(); ++i) {
if (windowDatas[i]->window == w)
return i;
}
return 0;
}
size_t SColumnData::idxForHeight(float y) {
for (size_t i = 0; i < windowDatas.size(); ++i) {
if (windowDatas[i]->window->m_position.y < y)
continue;
return i - 1;
}
return windowDatas.size() - 1;
}
2025-05-02 19:48:37 +01:00
void SColumnData::remove(PHLWINDOW w) {
const auto SIZE_BEFORE = windowDatas.size();
2025-05-02 19:48:37 +01:00
std::erase_if(windowDatas, [&w](const auto& e) { return e->window == w; });
if (SIZE_BEFORE == windowDatas.size() && SIZE_BEFORE > 0)
return;
float newMaxSize = 0.F;
for (auto& wd : windowDatas) {
newMaxSize += wd->windowSize;
}
for (auto& wd : windowDatas) {
wd->windowSize *= 1.F / newMaxSize;
}
2025-05-02 19:48:37 +01:00
if (windowDatas.empty() && workspace)
workspace->remove(self.lock());
}
void SColumnData::up(SP<SScrollingWindowData> w) {
for (size_t i = 1; i < windowDatas.size(); ++i) {
if (windowDatas[i] != w)
continue;
std::swap(windowDatas[i], windowDatas[i - 1]);
break;
2025-05-02 19:48:37 +01:00
}
}
void SColumnData::down(SP<SScrollingWindowData> w) {
for (size_t i = 0; i < windowDatas.size() - 1; ++i) {
if (windowDatas[i] != w)
continue;
std::swap(windowDatas[i], windowDatas[i + 1]);
break;
2025-05-02 19:48:37 +01:00
}
}
SP<SScrollingWindowData> SColumnData::next(SP<SScrollingWindowData> w) {
for (size_t i = 0; i < windowDatas.size() - 1; ++i) {
if (windowDatas[i] != w)
continue;
return windowDatas[i + 1];
}
return nullptr;
}
SP<SScrollingWindowData> SColumnData::prev(SP<SScrollingWindowData> w) {
for (size_t i = 1; i < windowDatas.size(); ++i) {
if (windowDatas[i] != w)
continue;
return windowDatas[i - 1];
}
return nullptr;
}
2025-05-22 15:05:40 +02:00
bool SColumnData::has(PHLWINDOW w) {
return std::ranges::find_if(windowDatas, [w](const auto& e) { return e->window == w; }) != windowDatas.end();
}
2025-05-02 19:48:37 +01:00
SP<SColumnData> SWorkspaceData::add() {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("plugin:hyprscrolling:column_width");
auto col = columns.emplace_back(makeShared<SColumnData>(self.lock()));
col->self = col;
col->columnWidth = *PCOLWIDTH;
return col;
}
SP<SColumnData> SWorkspaceData::add(int after) {
static const auto PCOLWIDTH = CConfigValue<Hyprlang::FLOAT>("plugin:hyprscrolling:column_width");
auto col = makeShared<SColumnData>(self.lock());
col->self = col;
col->columnWidth = *PCOLWIDTH;
columns.insert(columns.begin() + after + 1, col);
return col;
}
int64_t SWorkspaceData::idx(SP<SColumnData> c) {
for (size_t i = 0; i < columns.size(); ++i) {
if (columns[i] == c)
return i;
}
return -1;
}
2025-05-02 19:48:37 +01:00
void SWorkspaceData::remove(SP<SColumnData> c) {
std::erase(columns, c);
}
SP<SColumnData> SWorkspaceData::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> SWorkspaceData::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 SWorkspaceData::centerCol(SP<SColumnData> c) {
if (!c)
return;
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
PHLMONITOR PMONITOR = workspace->m_monitor.lock();
double currentLeft = 0;
2025-05-22 15:05:40 +02:00
const auto USABLE = layout->usableAreaFor(PMONITOR);
2025-05-02 19:48:37 +01:00
for (const auto& COL : columns) {
2025-05-22 15:05:40 +02:00
const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;
2025-05-02 19:48:37 +01:00
if (COL != c)
currentLeft += ITEM_WIDTH;
else {
2025-05-22 15:05:40 +02:00
leftOffset = currentLeft - (USABLE.w - ITEM_WIDTH) / 2.F;
2025-05-02 19:48:37 +01:00
return;
}
}
}
void SWorkspaceData::fitCol(SP<SColumnData> c) {
if (!c)
return;
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
PHLMONITOR PMONITOR = workspace->m_monitor.lock();
double currentLeft = 0;
const auto USABLE = layout->usableAreaFor(PMONITOR);
for (const auto& COL : columns) {
const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;
if (COL != c)
currentLeft += ITEM_WIDTH;
else {
leftOffset = std::clamp((double)leftOffset, currentLeft - USABLE.w + ITEM_WIDTH, currentLeft);
return;
}
}
}
void SWorkspaceData::centerOrFitCol(SP<SColumnData> c) {
if (!c)
return;
static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
if (*PFITMETHOD == 1)
fitCol(c);
else
centerCol(c);
}
2025-05-02 19:48:37 +01:00
SP<SColumnData> SWorkspaceData::atCenter() {
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
PHLMONITOR PMONITOR = workspace->m_monitor.lock();
double currentLeft = leftOffset;
2025-05-22 15:05:40 +02:00
const auto USABLE = layout->usableAreaFor(PMONITOR);
2025-05-02 19:48:37 +01:00
for (const auto& COL : columns) {
2025-05-22 15:05:40 +02:00
const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;
2025-05-02 19:48:37 +01:00
currentLeft += ITEM_WIDTH;
if (currentLeft >= PMONITOR->m_size.x / 2.0 - 2)
return COL;
}
return nullptr;
}
void SWorkspaceData::recalculate(bool forceInstant) {
2025-05-02 19:48:37 +01:00
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
if (!workspace) {
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace data");
2025-05-02 19:48:37 +01:00
return;
}
2025-05-22 15:05:40 +02:00
const auto MAX_WIDTH = maxWidth();
2025-05-02 19:48:37 +01:00
2025-05-22 15:05:40 +02:00
PHLMONITOR PMONITOR = workspace->m_monitor.lock();
2025-05-22 15:05:40 +02:00
const CBox USABLE = layout->usableAreaFor(PMONITOR);
2025-05-22 15:05:40 +02:00
double currentLeft = 0;
const double cameraLeft = MAX_WIDTH < USABLE.w ? std::round((MAX_WIDTH - USABLE.w) / 2.0) : leftOffset; // layout pixels
2025-05-02 19:48:37 +01:00
for (size_t i = 0; i < columns.size(); ++i) {
const auto& COL = columns[i];
double currentTop = 0.0;
const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;
2025-05-02 19:48:37 +01:00
for (const auto& WINDOW : COL->windowDatas) {
2025-05-22 15:05:40 +02:00
WINDOW->layoutBox =
2025-12-06 11:23:55 +00:00
CBox{currentLeft, currentTop, ITEM_WIDTH, WINDOW->windowSize * USABLE.h}.translate(PMONITOR->logicalBoxMinusReserved().pos() + Vector2D{-cameraLeft, 0.0});
2025-05-02 19:48:37 +01:00
currentTop += WINDOW->windowSize * USABLE.h;
2025-05-02 19:48:37 +01:00
layout->applyNodeDataToWindow(WINDOW, forceInstant, i != columns.size() - 1, i != 0);
2025-05-02 19:48:37 +01:00
}
currentLeft += ITEM_WIDTH;
if (currentLeft == USABLE.width)
currentLeft++; // avoid ffm from "grabbing" the window on the right
2025-05-02 19:48:37 +01:00
}
}
double SWorkspaceData::maxWidth() {
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
PHLMONITOR PMONITOR = workspace->m_monitor.lock();
double currentLeft = 0;
2025-05-22 15:05:40 +02:00
const auto USABLE = layout->usableAreaFor(PMONITOR);
2025-05-02 19:48:37 +01:00
for (const auto& COL : columns) {
2025-05-22 15:05:40 +02:00
const double ITEM_WIDTH = *PFSONONE && columns.size() == 1 ? USABLE.w : USABLE.w * COL->columnWidth;
2025-05-02 19:48:37 +01:00
currentLeft += ITEM_WIDTH;
}
return currentLeft;
}
2025-05-22 15:05:40 +02:00
bool SWorkspaceData::visible(SP<SColumnData> c) {
const auto USABLE = layout->usableAreaFor(workspace->m_monitor.lock());
float totalLeft = 0;
for (const auto& col : columns) {
if (col == c) {
const float colLeft = totalLeft;
const float colRight = totalLeft + col->columnWidth * USABLE.w;
const float viewLeft = leftOffset;
const float viewRight = leftOffset + USABLE.w;
return colLeft < viewRight && viewLeft < colRight;
}
2025-05-22 15:05:40 +02:00
totalLeft += col->columnWidth * USABLE.w;
}
return false;
}
void CScrollingLayout::applyNodeDataToWindow(SP<SScrollingWindowData> data, bool force, bool hasWindowsRight, bool hasWindowsLeft) {
2025-05-22 14:03:19 +02:00
PHLMONITOR PMONITOR;
PHLWORKSPACE PWORKSPACE;
2025-05-02 19:48:37 +01:00
if (!data || !data->column || !data->column->workspace) {
2025-05-22 14:03:19 +02:00
if (!data->overrideWorkspace) {
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace (1)");
2025-05-22 14:03:19 +02:00
return;
}
2025-05-02 19:48:37 +01:00
2025-05-22 14:03:19 +02:00
PMONITOR = data->overrideWorkspace->m_monitor.lock();
PWORKSPACE = data->overrideWorkspace.lock();
} else {
PMONITOR = data->column->workspace->workspace->m_monitor.lock();
PWORKSPACE = data->column->workspace->workspace.lock();
}
2025-05-02 19:48:37 +01:00
2025-05-22 14:03:19 +02:00
if (!PMONITOR || !PWORKSPACE) {
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::ERR, "[scroller] broken internal state on workspace (2)");
2025-05-02 19:48:37 +01:00
return;
}
// for gaps outer
2025-12-06 11:23:55 +00:00
const auto WORKAREA = PMONITOR->logicalBoxMinusReserved();
const bool DISPLAYLEFT = !hasWindowsLeft;
const bool DISPLAYRIGHT = !hasWindowsRight;
2025-12-06 11:23:55 +00:00
const bool DISPLAYTOP = STICKS(data->layoutBox.y, WORKAREA.y);
const bool DISPLAYBOTTOM = STICKS(data->layoutBox.y + data->layoutBox.h, WORKAREA.y + WORKAREA.h);
2025-05-02 19:48:37 +01:00
const auto PWINDOW = data->window.lock();
// get specific gaps and rules for this workspace,
// if user specified them in config
2025-05-22 14:03:19 +02:00
const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(PWORKSPACE);
2025-05-02 19:48:37 +01:00
if (!validMapped(PWINDOW)) {
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::ERR, "Node {} holding invalid {}!!", (uintptr_t)data.get(), PWINDOW);
2025-05-02 19:48:37 +01:00
onWindowRemovedTiling(PWINDOW);
return;
}
if (PWINDOW->isFullscreen() && !data->ignoreFullscreenChecks)
2025-05-02 19:48:37 +01:00
return;
2025-11-19 20:07:46 +01:00
PWINDOW->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT);
2025-05-02 19:48:37 +01:00
PWINDOW->updateWindowData();
static auto PGAPSINDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_in");
static auto PGAPSOUTDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_out");
auto* const PGAPSIN = (CCssGapData*)(PGAPSINDATA.ptr())->getData();
auto* const PGAPSOUT = (CCssGapData*)(PGAPSOUTDATA.ptr())->getData();
auto gapsIn = WORKSPACERULE.gapsIn.value_or(*PGAPSIN);
CBox nodeBox = data->layoutBox;
nodeBox.round();
PWINDOW->m_size = nodeBox.size();
PWINDOW->m_position = nodeBox.pos();
PWINDOW->updateWindowDecos();
auto calcPos = PWINDOW->m_position;
auto calcSize = PWINDOW->m_size;
2025-12-06 11:23:55 +00:00
const auto OFFSETTOPLEFT = Vector2D((double)(DISPLAYLEFT ? 0 : gapsIn.m_left), (double)(DISPLAYTOP ? 0 : gapsIn.m_top));
2025-05-02 19:48:37 +01:00
2025-12-06 11:23:55 +00:00
const auto OFFSETBOTTOMRIGHT = Vector2D((double)(DISPLAYRIGHT ? 0 : gapsIn.m_right), (double)(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom));
2025-05-02 19:48:37 +01:00
calcPos = calcPos + OFFSETTOPLEFT;
calcSize = calcSize - OFFSETTOPLEFT - OFFSETBOTTOMRIGHT;
if (PWINDOW->m_isPseudotiled) {
// Calculate pseudo
float scale = 1;
// adjust if doesnt fit
if (PWINDOW->m_pseudoSize.x > calcSize.x || PWINDOW->m_pseudoSize.y > calcSize.y) {
if (PWINDOW->m_pseudoSize.x > calcSize.x) {
scale = calcSize.x / PWINDOW->m_pseudoSize.x;
}
if (PWINDOW->m_pseudoSize.y * scale > calcSize.y) {
scale = calcSize.y / PWINDOW->m_pseudoSize.y;
}
auto DELTA = calcSize - PWINDOW->m_pseudoSize * scale;
calcSize = PWINDOW->m_pseudoSize * scale;
calcPos = calcPos + DELTA / 2.f; // center
} else {
auto DELTA = calcSize - PWINDOW->m_pseudoSize;
calcPos = calcPos + DELTA / 2.f; // center
calcSize = PWINDOW->m_pseudoSize;
}
}
const auto RESERVED = PWINDOW->getFullWindowReservedArea();
calcPos = calcPos + RESERVED.topLeft;
calcSize = calcSize - (RESERVED.topLeft + RESERVED.bottomRight);
if (PWINDOW->onSpecialWorkspace() && !PWINDOW->isFullscreen()) {
// if special, we adjust the coords a bit
static auto PSCALEFACTOR = CConfigValue<Hyprlang::FLOAT>("dwindle:special_scale_factor");
CBox wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR};
wb.round(); // avoid rounding mess
*PWINDOW->m_realPosition = wb.pos();
*PWINDOW->m_realSize = wb.size();
} else {
CBox wb = {calcPos, calcSize};
wb.round(); // avoid rounding mess
*PWINDOW->m_realSize = wb.size();
*PWINDOW->m_realPosition = wb.pos();
}
if (force) {
g_pHyprRenderer->damageWindow(PWINDOW);
PWINDOW->m_realPosition->warp();
PWINDOW->m_realSize->warp();
g_pHyprRenderer->damageWindow(PWINDOW);
}
PWINDOW->updateWindowDecos();
}
void CScrollingLayout::onEnable() {
static const auto PCONFWIDTHS = CConfigValue<Hyprlang::STRING>("plugin:hyprscrolling:explicit_column_widths");
m_configCallback = g_pHookSystem->hookDynamic("configReloaded", [this](void* hk, SCallbackInfo& info, std::any param) {
// bitch ass
m_config.configuredWidths.clear();
CConstVarList widths(*PCONFWIDTHS, 0, ',');
for (auto& w : widths) {
try {
m_config.configuredWidths.emplace_back(std::stof(std::string{w}));
2025-12-19 16:24:43 +00:00
} catch (...) { Log::logger->log(Log::ERR, "scrolling: Failed to parse width {} as float", w); }
}
});
2025-07-11 18:59:49 +02:00
m_focusCallback = g_pHookSystem->hookDynamic("activeWindow", [this](void* hk, SCallbackInfo& info, std::any param) {
const auto PWINDOW = std::any_cast<PHLWINDOW>(param);
if (!PWINDOW)
return;
static const auto PFOLLOW_FOCUS = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:follow_focus");
if (!*PFOLLOW_FOCUS)
return;
if (!PWINDOW->m_workspace->isVisible())
return;
const auto DATA = dataFor(PWINDOW->m_workspace);
2025-07-11 18:59:49 +02:00
const auto WINDOWDATA = dataFor(PWINDOW);
if (!DATA || !WINDOWDATA)
return;
DATA->fitCol(WINDOWDATA->column.lock());
DATA->recalculate();
});
2025-05-02 19:48:37 +01:00
for (auto const& w : g_pCompositor->m_windows) {
if (w->m_isFloating || !w->m_isMapped || w->isHidden())
continue;
onWindowCreatedTiling(w);
}
}
void CScrollingLayout::onDisable() {
m_workspaceDatas.clear();
m_configCallback.reset();
2025-05-02 19:48:37 +01:00
}
void CScrollingLayout::onWindowCreatedTiling(PHLWINDOW window, eDirection direction) {
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
if (m_columnMoveState.isMovingColumn && window->m_workspace->m_id == m_columnMoveState.targetWorkspaceID)
return;
2025-05-02 19:48:37 +01:00
auto workspaceData = dataFor(window->m_workspace);
if (!workspaceData) {
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::DEBUG, "[scrolling] No workspace data yet, creating");
2025-05-02 19:48:37 +01:00
workspaceData = m_workspaceDatas.emplace_back(makeShared<SWorkspaceData>(window->m_workspace, this));
workspaceData->self = workspaceData;
}
auto droppingOn = Desktop::focusState()->window();
2025-05-02 19:48:37 +01:00
if (droppingOn == window)
2025-12-08 15:22:46 +00:00
droppingOn = g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS);
2025-05-02 19:48:37 +01:00
SP<SScrollingWindowData> droppingData = droppingOn ? dataFor(droppingOn) : nullptr;
SP<SColumnData> droppingColumn = droppingData ? droppingData->column.lock() : nullptr;
2025-05-02 19:48:37 +01:00
2025-12-19 16:24:43 +00:00
Log::logger->log(Log::DEBUG, "[scrolling] new window {:x}, droppingColumn: {:x}, columns before: {}", (uintptr_t)window.get(), (uintptr_t)droppingColumn.get(),
workspaceData->columns.size());
2025-05-02 19:48:37 +01:00
if (!droppingColumn) {
2025-05-02 19:48:37 +01:00
auto col = workspaceData->add();
col->add(window);
workspaceData->fitCol(col);
2025-05-02 19:48:37 +01:00
} else {
if (window->m_draggingTiled) {
if (droppingOn) {
const auto IDX = droppingColumn->idx(droppingOn);
const auto TOP = droppingOn->getWindowIdealBoundingBoxIgnoreReserved().middle().y > g_pInputManager->getMouseCoordsInternal().y;
droppingColumn->add(window, TOP ? (IDX == 0 ? -1 : IDX - 1) : (IDX));
} else
droppingColumn->add(window);
workspaceData->fitCol(droppingColumn);
} else {
auto idx = workspaceData->idx(droppingColumn);
auto col = idx == -1 ? workspaceData->add() : workspaceData->add(idx);
col->add(window);
workspaceData->fitCol(col);
}
2025-05-02 19:48:37 +01:00
}
workspaceData->recalculate();
}
void CScrollingLayout::onWindowRemovedTiling(PHLWINDOW window) {
const auto DATA = dataFor(window);
if (!DATA)
return;
const auto WS = DATA->column->workspace.lock();
if (!WS->next(DATA->column.lock())) {
// move the view if this is the last column
const auto USABLE = usableAreaFor(window->m_monitor.lock());
WS->leftOffset -= USABLE.w * DATA->column->columnWidth;
}
2025-05-02 19:48:37 +01:00
DATA->column->remove(window);
WS->recalculate();
if (!DATA->column) {
// column got removed, let's ensure we don't leave any cringe extra space
const auto USABLE = usableAreaFor(window->m_monitor.lock());
WS->leftOffset = std::clamp((double)WS->leftOffset, 0.0, std::max(WS->maxWidth() - USABLE.w, 1.0));
}
2025-05-02 19:48:37 +01:00
}
bool CScrollingLayout::isWindowTiled(PHLWINDOW window) {
const auto DATA = dataFor(window);
return DATA;
}
void CScrollingLayout::recalculateMonitor(const MONITORID& id) {
const auto PMONITOR = g_pCompositor->getMonitorFromID(id);
if (!PMONITOR || !PMONITOR->m_activeWorkspace)
return;
const auto DATA = dataFor(PMONITOR->m_activeWorkspace);
if (!DATA)
return;
DATA->recalculate();
}
void CScrollingLayout::recalculateWindow(PHLWINDOW window) {
if (!window->m_workspace)
return;
const auto DATA = dataFor(window->m_workspace);
if (!DATA)
return;
DATA->recalculate();
}
void CScrollingLayout::onBeginDragWindow() {
IHyprLayout::onBeginDragWindow();
}
void CScrollingLayout::resizeActiveWindow(const Vector2D& delta, eRectCorner corner, PHLWINDOW pWindow) {
const auto PWINDOW = pWindow ? pWindow : Desktop::focusState()->window();
Vector2D modDelta = delta;
if (!validMapped(PWINDOW))
return;
const auto DATA = dataFor(PWINDOW);
if (!DATA) {
2025-12-06 11:23:55 +00:00
*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->workspace || !DATA->column->workspace->workspace || !DATA->column->workspace->workspace->m_monitor)
return;
const auto USABLE = usableAreaFor(DATA->column->workspace->workspace->m_monitor.lock());
const auto DELTA_AS_PERC = delta / USABLE.size();
const auto CURR_COLUMN = DATA->column.lock();
const auto NEXT_COLUMN = DATA->column->workspace->next(CURR_COLUMN);
const auto PREV_COLUMN = DATA->column->workspace->prev(CURR_COLUMN);
switch (corner) {
case CORNER_BOTTOMLEFT:
case CORNER_TOPLEFT: {
if (!PREV_COLUMN)
break;
PREV_COLUMN->columnWidth = std::clamp(PREV_COLUMN->columnWidth + (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
CURR_COLUMN->columnWidth = std::clamp(CURR_COLUMN->columnWidth - (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
break;
}
case CORNER_BOTTOMRIGHT:
case CORNER_TOPRIGHT: {
if (!NEXT_COLUMN)
break;
NEXT_COLUMN->columnWidth = std::clamp(NEXT_COLUMN->columnWidth - (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
CURR_COLUMN->columnWidth = std::clamp(CURR_COLUMN->columnWidth + (float)DELTA_AS_PERC.x, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
break;
}
default: break;
}
if (DATA->column->windowDatas.size() > 1) {
const auto CURR_WD = DATA;
const auto NEXT_WD = DATA->column->next(DATA);
const auto PREV_WD = DATA->column->prev(DATA);
if (corner == CORNER_NONE) {
if (!PREV_WD)
corner = CORNER_BOTTOMRIGHT;
else {
corner = CORNER_TOPRIGHT;
modDelta.y *= -1.0f;
}
}
switch (corner) {
case CORNER_BOTTOMLEFT:
case CORNER_BOTTOMRIGHT: {
if (!NEXT_WD)
break;
if (NEXT_WD->windowSize <= MIN_ROW_HEIGHT && delta.y >= 0)
break;
float adjust = std::clamp((float)(delta.y / USABLE.h), (-CURR_WD->windowSize + MIN_ROW_HEIGHT), (NEXT_WD->windowSize - MIN_ROW_HEIGHT));
NEXT_WD->windowSize = std::clamp(NEXT_WD->windowSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
CURR_WD->windowSize = std::clamp(CURR_WD->windowSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
break;
}
case CORNER_TOPLEFT:
case CORNER_TOPRIGHT: {
if (!PREV_WD)
break;
if (PREV_WD->windowSize <= MIN_ROW_HEIGHT && modDelta.y <= 0 || CURR_WD->windowSize <= MIN_ROW_HEIGHT && delta.y >= 0)
break;
float adjust = std::clamp((float)(modDelta.y / USABLE.h), -(PREV_WD->windowSize - MIN_ROW_HEIGHT), (CURR_WD->windowSize - MIN_ROW_HEIGHT));
PREV_WD->windowSize = std::clamp(PREV_WD->windowSize + adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
CURR_WD->windowSize = std::clamp(CURR_WD->windowSize - adjust, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
break;
}
default: break;
}
}
DATA->column->workspace->recalculate(true);
2025-05-02 19:48:37 +01:00
}
void CScrollingLayout::fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) {
2025-05-22 14:03:19 +02:00
const auto PMONITOR = pWindow->m_monitor.lock();
const auto PWORKSPACE = pWindow->m_workspace;
// save position and size if floating
if (pWindow->m_isFloating && CURRENT_EFFECTIVE_MODE == FSMODE_NONE) {
pWindow->m_lastFloatingSize = pWindow->m_realSize->goal();
pWindow->m_lastFloatingPosition = pWindow->m_realPosition->goal();
pWindow->m_position = pWindow->m_realPosition->goal();
pWindow->m_size = pWindow->m_realSize->goal();
}
const auto PNODE = dataFor(pWindow);
if (EFFECTIVE_MODE == FSMODE_NONE) {
// if it got its fullscreen disabled, set back its node if it had one
if (PNODE)
applyNodeDataToWindow(PNODE, false, false, false);
2025-05-22 14:03:19 +02:00
else {
// get back its' dimensions from position and size
*pWindow->m_realPosition = pWindow->m_lastFloatingPosition;
*pWindow->m_realSize = pWindow->m_lastFloatingSize;
2025-11-19 20:07:46 +01:00
pWindow->m_ruleApplicator->resetProps(Desktop::Rule::RULE_PROP_ALL, Desktop::Types::PRIORITY_LAYOUT);
2025-05-22 14:03:19 +02:00
pWindow->updateWindowData();
}
} else {
// apply new pos and size being monitors' box
if (EFFECTIVE_MODE == FSMODE_FULLSCREEN) {
*pWindow->m_realPosition = PMONITOR->m_position;
*pWindow->m_realSize = PMONITOR->m_size;
} else {
// This is a massive hack.
// We make a fake "only" node and apply
// To keep consistent with the settings without C+P code
SP<SScrollingWindowData> fakeNode = makeShared<SScrollingWindowData>(pWindow, nullptr);
fakeNode->window = pWindow;
2025-12-06 11:23:55 +00:00
fakeNode->layoutBox = PMONITOR->logicalBoxMinusReserved();
pWindow->m_size = fakeNode->layoutBox.size();
fakeNode->ignoreFullscreenChecks = true;
fakeNode->overrideWorkspace = pWindow->m_workspace;
2025-05-22 14:03:19 +02:00
applyNodeDataToWindow(fakeNode, false, false, false);
2025-05-22 14:03:19 +02:00
}
}
g_pCompositor->changeWindowZOrder(pWindow, true);
2025-05-02 19:48:37 +01:00
}
void CScrollingLayout::focusWindowUpdate(PHLWINDOW pWindow) {
if (!validMapped(pWindow)) {
Desktop::focusState()->fullWindowFocus(nullptr);
return;
}
Desktop::focusState()->fullWindowFocus(pWindow);
const auto WINDOWDATA = dataFor(pWindow);
if (WINDOWDATA) {
if (auto col = WINDOWDATA->column.lock())
col->lastFocusedWindow = WINDOWDATA;
}
}
SP<SScrollingWindowData> CScrollingLayout::findBestNeighbor(SP<SScrollingWindowData> pCurrent, SP<SColumnData> pTargetCol) {
if (!pCurrent || !pTargetCol || pTargetCol->windowDatas.empty())
return nullptr;
const double currentTop = pCurrent->layoutBox.y;
const double currentBottom = pCurrent->layoutBox.y + pCurrent->layoutBox.h;
std::vector<SP<SScrollingWindowData>> overlappingWindows;
for (const auto& candidate : pTargetCol->windowDatas) {
const double candidateTop = candidate->layoutBox.y;
const double candidateBottom = candidate->layoutBox.y + candidate->layoutBox.h;
const bool overlaps = (candidateTop < currentBottom) && (candidateBottom > currentTop);
if (overlaps)
overlappingWindows.emplace_back(candidate);
}
if (!overlappingWindows.empty()) {
auto lastFocused = pTargetCol->lastFocusedWindow.lock();
if (lastFocused) {
auto it = std::ranges::find(overlappingWindows, lastFocused);
if (it != overlappingWindows.end())
return lastFocused;
}
auto topmost = std::ranges::min_element(overlappingWindows, std::less<>{}, [](const SP<SScrollingWindowData>& w) { return w->layoutBox.y; });
return *topmost;
}
if (!pTargetCol->windowDatas.empty())
return pTargetCol->windowDatas.front();
return nullptr;
}
2025-05-02 19:48:37 +01:00
std::any CScrollingLayout::layoutMessage(SLayoutMessageHeader header, std::string message) {
static auto centerOrFit = [](const SP<SWorkspaceData> WS, const SP<SColumnData> COL) -> void {
static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
if (*PFITMETHOD == 1)
WS->fitCol(COL);
else
WS->centerCol(COL);
};
2025-05-02 19:48:37 +01:00
const auto ARGS = CVarList(message, 0, ' ');
if (ARGS[0] == "move") {
const auto DATA = currentWorkspaceData();
if (!DATA)
return {};
if (ARGS[1] == "+col" || ARGS[1] == "col") {
const auto WDATA = dataFor(Desktop::focusState()->window());
2025-05-02 19:48:37 +01:00
if (!WDATA)
return {};
const auto COL = DATA->next(WDATA->column.lock());
if (!COL) {
// move to max
2025-05-22 15:05:40 +02:00
DATA->leftOffset = DATA->maxWidth();
2025-05-02 19:48:37 +01:00
DATA->recalculate();
focusWindowUpdate(nullptr);
2025-05-02 19:48:37 +01:00
return {};
}
centerOrFit(DATA, COL);
2025-05-02 19:48:37 +01:00
DATA->recalculate();
focusWindowUpdate(COL->windowDatas.front()->window.lock());
g_pCompositor->warpCursorTo(COL->windowDatas.front()->window.lock()->middle());
2025-05-02 19:48:37 +01:00
return {};
} else if (ARGS[1] == "-col") {
const auto WDATA = dataFor(Desktop::focusState()->window());
2025-05-02 19:48:37 +01:00
if (!WDATA) {
if (DATA->columns.size() > 0) {
2025-05-02 19:48:37 +01:00
DATA->centerCol(DATA->columns.back());
DATA->recalculate();
focusWindowUpdate((DATA->columns.back()->windowDatas.back())->window.lock());
g_pCompositor->warpCursorTo((DATA->columns.back()->windowDatas.back())->window.lock()->middle());
2025-05-02 19:48:37 +01:00
}
return {};
}
const auto COL = DATA->prev(WDATA->column.lock());
if (!COL)
return {};
centerOrFit(DATA, COL);
2025-05-02 19:48:37 +01:00
DATA->recalculate();
focusWindowUpdate(COL->windowDatas.back()->window.lock());
g_pCompositor->warpCursorTo(COL->windowDatas.front()->window.lock()->middle());
2025-05-02 19:48:37 +01:00
return {};
}
const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0);
if (!PLUSMINUS.has_value())
return {};
DATA->leftOffset -= *PLUSMINUS;
DATA->recalculate();
const auto ATCENTER = DATA->atCenter();
focusWindowUpdate(ATCENTER ? (*ATCENTER->windowDatas.begin())->window.lock() : nullptr);
2025-05-02 19:48:37 +01:00
} else if (ARGS[0] == "colresize") {
const auto WDATA = dataFor(Desktop::focusState()->window());
2025-05-02 19:48:37 +01:00
if (!WDATA)
return {};
if (ARGS[1] == "all") {
float abs = 0;
try {
abs = std::stof(ARGS[2]);
} catch (...) { return {}; }
for (const auto& c : WDATA->column->workspace->columns) {
c->columnWidth = abs;
}
WDATA->column->workspace->recalculate();
return {};
}
CScopeGuard x([WDATA] {
WDATA->column->columnWidth = std::clamp(WDATA->column->columnWidth, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
WDATA->column->workspace->centerOrFitCol(WDATA->column.lock());
WDATA->column->workspace->recalculate();
});
2025-05-02 19:48:37 +01:00
if (ARGS[1][0] == '+' || ARGS[1][0] == '-') {
if (ARGS[1] == "+conf") {
for (size_t i = 0; i < m_config.configuredWidths.size(); ++i) {
if (m_config.configuredWidths[i] > WDATA->column->columnWidth) {
WDATA->column->columnWidth = m_config.configuredWidths[i];
break;
}
if (i == m_config.configuredWidths.size() - 1)
WDATA->column->columnWidth = m_config.configuredWidths[0];
}
return {};
} else if (ARGS[1] == "-conf") {
for (size_t i = m_config.configuredWidths.size() - 1;; --i) {
if (m_config.configuredWidths[i] < WDATA->column->columnWidth) {
WDATA->column->columnWidth = m_config.configuredWidths[i];
break;
}
if (i == 0) {
WDATA->column->columnWidth = m_config.configuredWidths.back();
break;
}
}
return {};
}
2025-05-02 19:48:37 +01:00
const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0);
if (!PLUSMINUS.has_value())
return {};
WDATA->column->columnWidth += *PLUSMINUS;
} else {
float abs = 0;
try {
abs = std::stof(ARGS[1]);
} catch (...) { return {}; }
WDATA->column->columnWidth = abs;
}
} else if (ARGS[0] == "movewindowto") {
moveWindowTo(Desktop::focusState()->window(), ARGS[1], false);
2025-05-22 15:05:40 +02:00
} else if (ARGS[0] == "fit") {
if (ARGS[1] == "active") {
// fit the current column to 1.F
const auto WDATA = dataFor(Desktop::focusState()->window());
const auto WORKDATA = dataFor(Desktop::focusState()->window()->m_workspace);
2025-05-22 15:05:40 +02:00
if (!WDATA || !WORKDATA || WORKDATA->columns.size() == 0)
return {};
const auto USABLE = usableAreaFor(WORKDATA->workspace->m_monitor.lock());
WDATA->column->columnWidth = 1.F;
WORKDATA->leftOffset = 0;
for (size_t i = 0; i < WORKDATA->columns.size(); ++i) {
if (WORKDATA->columns[i]->has(Desktop::focusState()->window()))
2025-05-22 15:05:40 +02:00
break;
WORKDATA->leftOffset += USABLE.w * WORKDATA->columns[i]->columnWidth;
}
WDATA->column->workspace->recalculate();
} else if (ARGS[1] == "all") {
// fit all columns on screen
const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);
2025-05-22 15:05:40 +02:00
if (!WDATA || WDATA->columns.size() == 0)
return {};
const size_t LEN = WDATA->columns.size();
for (const auto& c : WDATA->columns) {
c->columnWidth = 1.F / (float)LEN;
}
WDATA->recalculate();
} else if (ARGS[1] == "toend") {
// fit all columns on screen that start from the current and end on the last
const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);
2025-05-22 15:05:40 +02:00
if (!WDATA || WDATA->columns.size() == 0)
return {};
bool begun = false;
size_t foundAt = 0;
for (size_t i = 0; i < WDATA->columns.size(); ++i) {
if (!begun && !WDATA->columns[i]->has(Desktop::focusState()->window()))
2025-05-22 15:05:40 +02:00
continue;
if (!begun) {
begun = true;
foundAt = i;
}
WDATA->columns[i]->columnWidth = 1.F / (float)(WDATA->columns.size() - i);
}
if (!begun)
return {};
const auto USABLE = usableAreaFor(WDATA->workspace->m_monitor.lock());
WDATA->leftOffset = 0;
for (size_t i = 0; i < foundAt; ++i) {
WDATA->leftOffset += USABLE.w * WDATA->columns[i]->columnWidth;
}
WDATA->recalculate();
} else if (ARGS[1] == "tobeg") {
// fit all columns on screen that start from the current and end on the last
const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);
2025-05-22 15:05:40 +02:00
if (!WDATA || WDATA->columns.size() == 0)
return {};
bool begun = false;
size_t foundAt = 0;
for (int64_t i = (int64_t)WDATA->columns.size() - 1; i >= 0; --i) {
if (!begun && !WDATA->columns[i]->has(Desktop::focusState()->window()))
2025-05-22 15:05:40 +02:00
continue;
if (!begun) {
begun = true;
foundAt = i;
}
WDATA->columns[i]->columnWidth = 1.F / (float)(foundAt + 1);
}
if (!begun)
return {};
WDATA->leftOffset = 0;
WDATA->recalculate();
} else if (ARGS[1] == "visible") {
// fit all columns on screen that start from the current and end on the last
const auto WDATA = dataFor(Desktop::focusState()->window()->m_workspace);
2025-05-22 15:05:40 +02:00
if (!WDATA || WDATA->columns.size() == 0)
return {};
bool begun = false;
size_t foundAt = 0;
std::vector<SP<SColumnData>> visible;
for (size_t i = 0; i < WDATA->columns.size(); ++i) {
if (!begun && !WDATA->visible(WDATA->columns[i]))
continue;
if (!begun) {
begun = true;
foundAt = i;
}
if (!WDATA->visible(WDATA->columns[i]))
break;
visible.emplace_back(WDATA->columns[i]);
}
if (!begun)
return {};
WDATA->leftOffset = 0;
if (foundAt != 0) {
const auto USABLE = usableAreaFor(WDATA->workspace->m_monitor.lock());
for (size_t i = 0; i < foundAt; ++i) {
WDATA->leftOffset += USABLE.w * WDATA->columns[i]->columnWidth;
}
}
for (const auto& v : visible) {
v->columnWidth = 1.F / (float)visible.size();
}
WDATA->recalculate();
}
} else if (ARGS[0] == "focus") {
const auto WDATA = dataFor(Desktop::focusState()->window());
static const auto PNOFALLBACK = CConfigValue<Hyprlang::INT>("general:no_focus_fallback");
if (!WDATA || ARGS[1].empty())
return {};
switch (ARGS[1][0]) {
case 'u':
case 't': {
auto PREV = WDATA->column->prev(WDATA);
if (!PREV) {
if (*PNOFALLBACK)
break;
else
PREV = WDATA->column->windowDatas.back();
}
focusWindowUpdate(PREV->window.lock());
g_pCompositor->warpCursorTo(PREV->window.lock()->middle());
break;
}
case 'b':
case 'd': {
auto NEXT = WDATA->column->next(WDATA);
if (!NEXT) {
if (*PNOFALLBACK)
break;
else
NEXT = WDATA->column->windowDatas.front();
}
focusWindowUpdate(NEXT->window.lock());
g_pCompositor->warpCursorTo(NEXT->window.lock()->middle());
break;
}
case 'l': {
auto PREV = WDATA->column->workspace->prev(WDATA->column.lock());
if (!PREV) {
if (*PNOFALLBACK) {
centerOrFit(WDATA->column->workspace.lock(), WDATA->column.lock());
WDATA->column->workspace->recalculate();
g_pCompositor->warpCursorTo(WDATA->window.lock()->middle());
break;
} else
PREV = WDATA->column->workspace->columns.back();
}
auto pTargetWindowData = findBestNeighbor(WDATA, PREV);
if (pTargetWindowData) {
focusWindowUpdate(pTargetWindowData->window.lock());
centerOrFit(WDATA->column->workspace.lock(), PREV);
WDATA->column->workspace->recalculate();
g_pCompositor->warpCursorTo(pTargetWindowData->window.lock()->middle());
}
break;
}
case 'r': {
auto NEXT = WDATA->column->workspace->next(WDATA->column.lock());
if (!NEXT) {
if (*PNOFALLBACK) {
centerOrFit(WDATA->column->workspace.lock(), WDATA->column.lock());
WDATA->column->workspace->recalculate();
g_pCompositor->warpCursorTo(WDATA->window.lock()->middle());
break;
} else
NEXT = WDATA->column->workspace->columns.front();
}
auto pTargetWindowData = findBestNeighbor(WDATA, NEXT);
if (pTargetWindowData) {
focusWindowUpdate(pTargetWindowData->window.lock());
centerOrFit(WDATA->column->workspace.lock(), NEXT);
WDATA->column->workspace->recalculate();
g_pCompositor->warpCursorTo(pTargetWindowData->window.lock()->middle());
}
break;
}
default: return {};
}
2025-05-24 14:31:35 +02:00
} else if (ARGS[0] == "promote") {
const auto WDATA = dataFor(Desktop::focusState()->window());
2025-05-24 14:31:35 +02:00
if (!WDATA)
return {};
auto idx = WDATA->column->workspace->idx(WDATA->column.lock());
auto col = idx == -1 ? WDATA->column->workspace->add() : WDATA->column->workspace->add(idx);
WDATA->column->remove(WDATA->window.lock());
col->add(WDATA);
WDATA->column->workspace->recalculate();
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
} else if (ARGS[0] == "swapcol") {
if (ARGS.size() < 2)
return {};
const auto WDATA = dataFor(Desktop::focusState()->window());
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
if (!WDATA)
return {};
const auto CURRENT_COL = WDATA->column.lock();
if (!CURRENT_COL)
return {};
const auto WS_DATA = CURRENT_COL->workspace.lock();
if (!WS_DATA || WS_DATA->columns.size() < 2)
return {};
const int64_t current_idx = WS_DATA->idx(CURRENT_COL);
const size_t col_count = WS_DATA->columns.size();
if (current_idx == -1)
return {};
const std::string& direction = ARGS[1];
int64_t target_idx = -1;
if (direction == "l")
target_idx = (current_idx == 0) ? (col_count - 1) : (current_idx - 1);
else if (direction == "r")
target_idx = (current_idx == (int64_t)col_count - 1) ? 0 : (current_idx + 1);
else
return {};
std::swap(WS_DATA->columns[current_idx], WS_DATA->columns[target_idx]);
WS_DATA->centerOrFitCol(CURRENT_COL);
WS_DATA->recalculate();
} else if (ARGS[0] == "movecoltoworkspace") {
if (ARGS.size() < 2)
return {};
const auto WDATA = dataFor(Desktop::focusState()->window());
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
if (!WDATA)
return {};
const auto CURRENT_COL = WDATA->column.lock();
if (!CURRENT_COL)
return {};
const auto SOURCE_WS_DATA = CURRENT_COL->workspace.lock();
if (!SOURCE_WS_DATA)
return {};
const auto PMONITOR = Desktop::focusState()->monitor();
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
if (!PMONITOR)
return {};
2025-05-02 19:48:37 +01:00
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
PHLWORKSPACE PWORKSPACE = nullptr;
const std::string& arg = ARGS[1];
if (arg.starts_with("+") || arg.starts_with("-")) {
try {
const int offset = std::stoi(arg);
const int currentWorkspaceID = WDATA->window->m_workspace->m_id;
const int targetWorkspaceID = currentWorkspaceID + offset;
if (targetWorkspaceID < 1)
return {};
PWORKSPACE = g_pCompositor->getWorkspaceByID(targetWorkspaceID);
if (!PWORKSPACE)
PWORKSPACE = g_pCompositor->createNewWorkspace(targetWorkspaceID, PMONITOR->m_id);
} catch (...) { return {}; }
} else if (arg == "special") {
const int SPECIAL_WORKSPACE_ID = -99;
PWORKSPACE = g_pCompositor->getWorkspaceByID(SPECIAL_WORKSPACE_ID);
if (!PWORKSPACE)
PWORKSPACE = g_pCompositor->createNewWorkspace(SPECIAL_WORKSPACE_ID, PMONITOR->m_id, "special");
} else {
PWORKSPACE = g_pCompositor->getWorkspaceByString(arg);
if (!PWORKSPACE) {
try {
const int workspaceID = std::stoi(arg);
PWORKSPACE = g_pCompositor->getWorkspaceByID(workspaceID);
if (!PWORKSPACE)
PWORKSPACE = g_pCompositor->createNewWorkspace(workspaceID, PMONITOR->m_id);
} catch (const std::invalid_argument&) { PWORKSPACE = g_pCompositor->createNewWorkspace(0, PMONITOR->m_id, arg); } catch (const std::out_of_range&) {
return {};
}
}
}
if (!PWORKSPACE)
return {};
if (PWORKSPACE == WDATA->window->m_workspace)
return {};
auto targetWorkspaceData = dataFor(PWORKSPACE);
if (!targetWorkspaceData) {
targetWorkspaceData = m_workspaceDatas.emplace_back(makeShared<SWorkspaceData>(PWORKSPACE, this));
targetWorkspaceData->self = targetWorkspaceData;
}
const auto NEW_COL = targetWorkspaceData->add();
NEW_COL->columnWidth = CURRENT_COL->columnWidth;
NEW_COL->windowDatas = CURRENT_COL->windowDatas;
for (const auto& wd : NEW_COL->windowDatas) {
wd->column = NEW_COL;
}
std::vector<PHLWINDOW> windowsToMove;
for (const auto& wd : CURRENT_COL->windowDatas) {
windowsToMove.push_back(wd->window.lock());
}
CURRENT_COL->windowDatas.clear();
SOURCE_WS_DATA->remove(CURRENT_COL);
CScopeGuard sg([this]() {
m_columnMoveState.isMovingColumn = false;
m_columnMoveState.targetWorkspaceID = -1;
for (auto& ws : m_workspaceDatas) {
ws->recalculate();
}
});
m_columnMoveState.isMovingColumn = true;
m_columnMoveState.targetWorkspaceID = PWORKSPACE->m_id;
for (const auto& win : windowsToMove) {
g_pCompositor->moveWindowToWorkspaceSafe(win, PWORKSPACE);
}
Desktop::focusState()->fullWindowFocus(windowsToMove.front());
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
g_pCompositor->warpCursorTo(windowsToMove.front()->middle());
} else if (ARGS[0] == "togglefit") {
static const auto PFITMETHOD = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:focus_fit_method");
auto& fitMethod = *PFITMETHOD.ptr();
const int toggled = fitMethod ^ 1;
fitMethod = toggled;
const auto focusedData = dataFor(Desktop::focusState()->window());
static const auto PFSONONE = CConfigValue<Hyprlang::INT>("plugin:hyprscrolling:fullscreen_on_one_column");
for (const auto& ws : m_workspaceDatas) {
if (!ws || ws->columns.empty())
continue;
const auto monitor = ws->workspace->m_monitor.lock();
if (!monitor)
continue;
const auto USABLE = usableAreaFor(monitor);
const auto focusedColumn = (focusedData && focusedData->column && focusedData->column->workspace.lock() == ws) ? focusedData->column.lock() : nullptr;
const auto fallbackColumn = ws->atCenter();
if (toggled == 1) {
const auto columnToFit = focusedColumn ? focusedColumn : fallbackColumn;
if (!columnToFit)
continue;
double currentLeft = 0.0;
for (const auto& col : ws->columns) {
const double itemWidth = *PFSONONE && ws->columns.size() == 1 ? USABLE.w : USABLE.w * col->columnWidth;
if (col == columnToFit) {
const double colLeft = currentLeft;
const double colRight = currentLeft + itemWidth;
const double scrollMax = std::max(ws->maxWidth() - USABLE.w, 0.0);
double desiredOffset;
if (col == ws->columns.front())
desiredOffset = 0.0;
else
desiredOffset = std::clamp(colRight - USABLE.w, 0.0, scrollMax);
ws->leftOffset = desiredOffset;
break;
}
currentLeft += itemWidth;
}
} else {
const auto columnToCenter = focusedColumn ? focusedColumn : fallbackColumn;
if (!columnToCenter)
continue;
ws->centerCol(columnToCenter);
}
ws->recalculate();
}
Hyprscrolling: feat: Add column movement commands (`swapcol`, `movecoltoworkspace`) (#481) * hyprscrolling: feat(layout) Add `swapcol` command for cyclic column swapping This commit introduces a new layout message, `swapcol`, to allow users to efficiently reorder entire columns within the scrolling layout like niri. The `swapcol` command accepts two arguments: - `l`: Swaps the current column with the one to its left. - `r`: Swaps the current column with the one to its right. A key feature of this implementation is its cyclic (wrap-around) behavior. When the leftmost column is swapped left, it moves to the end of the list. Similarly, swapping the rightmost column right moves it to the beginning. This provides a fluid and powerful way to rearrange the workspace without getting stuck at the edges. The README has also been updated to document this new functionality. Example usage in `hyprland.conf`: # Swap current column with the one to the left bind = $mainMod, H, layoutmsg, swapcol l # Swap current column with the one to the right bind = $mainMod, L, layoutmsg, swapcol r * Hyprscrolling: feat(layout) feat(layout): Add `movecoltoworkspace` command This commit introduces the `movecoltoworkspace` layout message, enabling users to move an entire column of windows to another workspace with a single command. This feature preserves the internal layout, window arrangement, and relative sizes within the column upon moving. It robustly handles moving to any workspace, including empty ones, by ensuring the target workspace is set to the 'scrolling' layout before the move. This addresses a key aspect of the feature request in issue #431, specifically the need for commands to move columns between workspaces. The README has been updated to document this new command. Example usage: # Move current column to workspace 2 bind = $mainMod, SHIFT, 2, layoutmsg, movecoltoworkspace 2 # Move current column to the next workspace bind = $mainMod, SHIFT, L, layoutmsg, movecoltoworkspace +1 * Hyprscrolling: fix: remove `{}` from short `if, for`s * Hyprscrolling: style: remove `{}` for short `if`. Add it back from `for`. * Hyprscrolling: clang-format
2025-09-20 23:33:04 +08:00
}
2025-05-02 19:48:37 +01:00
return {};
}
SWindowRenderLayoutHints CScrollingLayout::requestRenderHints(PHLWINDOW a) {
return {};
}
void CScrollingLayout::switchWindows(PHLWINDOW a, PHLWINDOW b) {
const auto DATA1 = dataFor(a);
const auto DATA2 = dataFor(b);
std::swap(DATA1->window, DATA2->window);
const auto WS1 = DATA1->column->workspace.lock();
const auto WS2 = DATA2->column->workspace.lock();
WS1->recalculate();
if (WS1 == WS2)
return;
WS2->recalculate();
2025-05-02 19:48:37 +01:00
}
void CScrollingLayout::moveWindowTo(PHLWINDOW w, const std::string& dir, bool silent) {
const auto DATA = dataFor(w);
if (!DATA)
return;
const auto WS = DATA->column->workspace.lock();
if (dir == "l") {
const auto COL = WS->prev(DATA->column.lock());
DATA->column->remove(w);
if (!COL) {
const auto NEWCOL = WS->add(-1);
NEWCOL->add(DATA);
WS->centerOrFitCol(NEWCOL);
} else {
if (COL->windowDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
WS->centerOrFitCol(COL);
}
2025-05-02 19:48:37 +01:00
} else if (dir == "r") {
const auto COL = WS->next(DATA->column.lock());
DATA->column->remove(w);
if (!COL) {
// make a new one
const auto NEWCOL = WS->add();
NEWCOL->add(DATA);
WS->centerOrFitCol(NEWCOL);
} else {
if (COL->windowDatas.size() > 0)
COL->add(DATA, COL->idxForHeight(g_pInputManager->getMouseCoordsInternal().y));
else
COL->add(DATA);
WS->centerOrFitCol(COL);
}
2025-05-02 19:48:37 +01:00
} else if (dir == "t" || dir == "u")
DATA->column->up(DATA);
else if (dir == "b" || dir == "d")
DATA->column->down(DATA);
WS->recalculate();
focusWindowUpdate(w);
g_pCompositor->warpCursorTo(w->middle());
2025-05-02 19:48:37 +01:00
}
void CScrollingLayout::alterSplitRatio(PHLWINDOW, float, bool) {
;
}
std::string CScrollingLayout::getLayoutName() {
return "scrolling";
}
void CScrollingLayout::replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) {
;
}
Vector2D CScrollingLayout::predictSizeForNewWindowTiled() {
return Vector2D{};
}
SP<SWorkspaceData> CScrollingLayout::dataFor(PHLWORKSPACE ws) {
for (const auto& e : m_workspaceDatas) {
if (e->workspace != ws)
continue;
return e;
}
return nullptr;
}
SP<SScrollingWindowData> CScrollingLayout::dataFor(PHLWINDOW w) {
if (!w)
return nullptr;
for (const auto& e : m_workspaceDatas) {
if (e->workspace != w->m_workspace)
continue;
for (const auto& c : e->columns) {
for (const auto& d : c->windowDatas) {
if (d->window != w)
continue;
return d;
}
}
}
return nullptr;
}
SP<SWorkspaceData> CScrollingLayout::currentWorkspaceData() {
if (!Desktop::focusState()->monitor() || !Desktop::focusState()->monitor()->m_activeWorkspace)
2025-05-02 19:48:37 +01:00
return nullptr;
// FIXME: special
return dataFor(Desktop::focusState()->monitor()->m_activeWorkspace);
2025-05-02 19:48:37 +01:00
}
CBox CScrollingLayout::usableAreaFor(PHLMONITOR m) {
2025-12-06 11:23:55 +00:00
return m->logicalBoxMinusReserved().translate(-m->m_position);
}