summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-05-07 22:29:43 +0100
committerGitHub <noreply@github.com>2026-05-07 22:29:43 +0100
commit4c8c45d754c9956d611bbe373b3e33a7d5957d68 (patch)
treeab5c800d548901b79da2e53f9a5393e96b909d4f
parent39af3901b389f792a55176041dec9211c82ed1c9 (diff)
gestures: add scroll_move (#14063)
-rw-r--r--src/config/legacy/ConfigManager.cpp5
-rw-r--r--src/config/lua/bindings/LuaBindingsConfigRules.cpp7
-rw-r--r--src/config/values/ConfigValues.cpp3
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp155
-rw-r--r--src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp11
-rw-r--r--src/managers/input/trackpad/gestures/ScrollMoveGesture.cpp145
-rw-r--r--src/managers/input/trackpad/gestures/ScrollMoveGesture.hpp27
7 files changed, 349 insertions, 4 deletions
diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp
index 08bfe086..677202ce 100644
--- a/src/config/legacy/ConfigManager.cpp
+++ b/src/config/legacy/ConfigManager.cpp
@@ -57,6 +57,7 @@
#include "../../managers/input/trackpad/gestures/FloatGesture.hpp"
#include "../../managers/input/trackpad/gestures/FullscreenGesture.hpp"
#include "../../managers/input/trackpad/gestures/CursorZoomGesture.hpp"
+#include "../../managers/input/trackpad/gestures/ScrollMoveGesture.hpp"
#include "../../event/EventBus.hpp"
@@ -1984,7 +1985,9 @@ std::optional<std::string> CConfigManager::handleGesture(const std::string& comm
else if (data[startDataIdx] == "cursorZoom") {
result = g_pTrackpadGestures->addGesture(makeUnique<CCursorZoomTrackpadGesture>(std::string{data[startDataIdx + 1]}, std::string{data[startDataIdx + 2]}), fingerCount,
direction, modMask, deltaScale, disableInhibit);
- } else if (data[startDataIdx] == "unset")
+ } else if (data[startDataIdx] == "scrollMove")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CScrollMoveTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (data[startDataIdx] == "unset")
result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale, disableInhibit);
else
return std::format("Invalid gesture: {}", data[startDataIdx]);
diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
index 51548c04..44cf5607 100644
--- a/src/config/lua/bindings/LuaBindingsConfigRules.cpp
+++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
@@ -38,6 +38,7 @@
#include "../../../managers/input/trackpad/gestures/ResizeGesture.hpp"
#include "../../../managers/input/trackpad/gestures/SpecialWorkspaceGesture.hpp"
#include "../../../managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/ScrollMoveGesture.hpp"
#include "../../../managers/permissions/DynamicPermissionManager.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
@@ -721,7 +722,7 @@ static int hlWorkspaceRule(lua_State* L) {
static int hlGesture(lua_State* L) {
if (!lua_istable(L, 1))
- return Internal::configError(L, "hl.gesture: expected a table, e.g. { fingers = 3, direction = \"horizontal\", action = \"workspace\" }");
+ return Internal::configError(L, R"(hl.gesture: expected a table, e.g. { fingers = 3, direction = "horizontal", action = "workspace" })");
CLuaConfigInt fingersParser(0, 2, 9);
auto fingersErr = Internal::parseTableField(L, 1, "fingers", fingersParser);
@@ -842,8 +843,10 @@ static int hlGesture(lua_State* L) {
result = g_pTrackpadGestures->addGesture(makeUnique<CFloatTrackpadGesture>(mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (action == "fullscreen")
result = g_pTrackpadGestures->addGesture(makeUnique<CFullscreenTrackpadGesture>(mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
- else if (action == "cursorZoom")
+ else if (action == "cursor_zoom" || action == "cursorZoom")
result = g_pTrackpadGestures->addGesture(makeUnique<CCursorZoomTrackpadGesture>(zoomLevel, mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "scroll_move")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CScrollMoveTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (action == "unset")
result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale, disableInhibit);
else
diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp
index 73f2e9db..88269989 100644
--- a/src/config/values/ConfigValues.cpp
+++ b/src/config/values/ConfigValues.cpp
@@ -376,7 +376,8 @@ std::vector<SP<IValue>> Values::getConfigValues() {
MS<Bool>("gestures:workspace_swipe_forever", "if enabled, swiping will not clamp at the neighboring workspaces but continue to the further ones.", false),
MS<Bool>("gestures:workspace_swipe_use_r", "if enabled, swiping will use the r prefix instead of the m prefix for finding workspaces.", false),
MS<Int>("gestures:close_max_timeout", "Timeout for closing windows with the close gesture, in ms.", 1000, {.min = 10, .max = 2000}),
-
+ MS<Bool>("gestures:scrolling:move_snap_to_grid", "When releasing the scroll move gesture, whether it shoud try to snap to the grid.", true),
+ MS<Bool>("gestures:scrolling:move_snap_cursor", "When releasing the scroll move gesture, whether it shoud snap the cursor to the newly focused window.", true),
/*
* group:
*/
diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp
index 2914ae8b..c6cfbada 100644
--- a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp
+++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp
@@ -1852,6 +1852,161 @@ Config::ErrorResult CScrollingAlgorithm::layoutMsg(const std::string_view& sv) {
return {};
}
+void CScrollingAlgorithm::moveTape(float delta) {
+ if (delta == 0.F)
+ return;
+
+ m_scrollingData->controller->adjustOffset(-delta);
+ m_scrollingData->recalculate();
+}
+
+void CScrollingAlgorithm::moveTapeNormalized(double delta) {
+ const double primary = primaryViewportSize();
+ if (primary <= 0.0 || delta == 0.0)
+ return;
+
+ moveTape(delta * primary);
+}
+
+void CScrollingAlgorithm::snapToGrid() {
+ snapToProjectedOffset(normalizedTapeOffset());
+}
+
+SP<SColumnData> CScrollingAlgorithm::snapToProjectedOffset(double projectedNormalizedOffset) {
+ static const auto PFSONONE = CConfigValue<Config::INTEGER>("scrolling:fullscreen_on_one_column");
+ static const auto PFITMETHOD = CConfigValue<Config::INTEGER>("scrolling:focus_fit_method");
+
+ const auto USABLE = usableArea();
+ auto& controller = *m_scrollingData->controller;
+
+ if (controller.stripCount() == 0)
+ return nullptr;
+
+ const double usablePrimary = controller.isPrimaryHorizontal() ? USABLE.w : USABLE.h;
+ if (usablePrimary <= 0.0)
+ return nullptr;
+
+ const double maxExtent = controller.calculateMaxExtent(USABLE, *PFSONONE);
+ if (maxExtent <= 0.0)
+ return nullptr;
+
+ const double projectedOffset = projectedNormalizedOffset * usablePrimary;
+
+ double bestOffset = 0.0;
+ double bestDelta = 0.0;
+ double bestCenterDelta = 0.0;
+ size_t bestIndex = 0;
+ bool foundSnap = false;
+
+ auto centerOffsetFor = [&](size_t index) {
+ const double start = controller.calculateStripStart(index, USABLE, *PFSONONE);
+ const double size = controller.calculateStripSize(index, USABLE, *PFSONONE);
+
+ return start - (usablePrimary - size) / 2.0;
+ };
+
+ auto fitOffsetFor = [&](size_t index) {
+ const double start = controller.calculateStripStart(index, USABLE, *PFSONONE);
+ const double size = controller.calculateStripSize(index, USABLE, *PFSONONE);
+ const double lo = start - usablePrimary + size;
+ const double hi = start;
+
+ if (lo > hi)
+ return centerOffsetFor(index);
+
+ const double center = centerOffsetFor(index);
+ const double edge = projectedOffset < center ? lo : hi;
+
+ return std::abs(projectedOffset - center) <= std::abs(projectedOffset - edge) ? center : edge;
+ };
+
+ auto considerColumn = [&](size_t index) {
+ const double offset = *PFITMETHOD == 1 ? fitOffsetFor(index) : centerOffsetFor(index);
+ const double delta = std::abs(offset - projectedOffset);
+ const double start = controller.calculateStripStart(index, USABLE, *PFSONONE);
+ const double size = controller.calculateStripSize(index, USABLE, *PFSONONE);
+ const double centerDelta = std::abs((start + size / 2.0) - (projectedOffset + usablePrimary / 2.0));
+ const bool betterFit = delta < bestDelta;
+ const bool betterTieBreak = delta == bestDelta && centerDelta < bestCenterDelta;
+
+ if (!foundSnap || betterFit || betterTieBreak) {
+ bestOffset = offset;
+ bestDelta = delta;
+ bestCenterDelta = centerDelta;
+ bestIndex = index;
+ foundSnap = true;
+ }
+ };
+
+ for (size_t i = 0; i < controller.stripCount(); ++i)
+ considerColumn(i);
+
+ if (!foundSnap)
+ return nullptr;
+
+ controller.setOffset(bestOffset);
+ m_scrollingData->recalculate();
+
+ if (bestIndex < m_scrollingData->columns.size())
+ return m_scrollingData->columns[bestIndex];
+
+ return nullptr;
+}
+
+void CScrollingAlgorithm::focusColumn(SP<SColumnData> column) {
+ if (!column || column->targetDatas.empty()) {
+ focusTargetUpdate(nullptr);
+ return;
+ }
+
+ auto targetData = column->lastFocusedTarget.lock();
+
+ if (!targetData || targetData->column.lock() != column || !targetData->target || !Desktop::View::validMapped(targetData->target->window())) {
+ targetData = nullptr;
+
+ for (const auto& candidate : column->targetDatas) {
+ if (candidate->target && Desktop::View::validMapped(candidate->target->window())) {
+ targetData = candidate;
+ break;
+ }
+ }
+ }
+
+ focusTargetUpdate(targetData ? targetData->target.lock() : nullptr);
+}
+
+SP<SColumnData> CScrollingAlgorithm::getColumnAtViewportCenter() {
+ return m_scrollingData ? m_scrollingData->atCenter() : nullptr;
+}
+
+SP<SColumnData> CScrollingAlgorithm::currentColumn() {
+ auto focus = Desktop::focusState()->window();
+
+ if (!focus)
+ return nullptr;
+
+ auto data = dataFor(focus->layoutTarget());
+
+ if (!data)
+ return nullptr;
+
+ return data->column.lock();
+}
+
+double CScrollingAlgorithm::primaryViewportSize() {
+ const auto USABLE = usableArea();
+
+ return m_scrollingData->controller->isPrimaryHorizontal() ? USABLE.w : USABLE.h;
+}
+
+double CScrollingAlgorithm::normalizedTapeOffset() {
+ const double primary = primaryViewportSize();
+ if (primary <= 0.0)
+ return 0.0;
+
+ return m_scrollingData->controller->getOffset() / primary;
+}
+
std::optional<Vector2D> CScrollingAlgorithm::predictSizeForNewTarget() {
return std::nullopt;
}
diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp
index 19eb9ba6..a7c64aa8 100644
--- a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp
+++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp
@@ -114,6 +114,17 @@ namespace Layout::Tiled {
virtual SP<ITarget> layoutFullscreenTarget() const;
virtual bool layoutFullscreenCoversMonitor() const;
+ void moveTape(float delta);
+ void moveTapeNormalized(double delta);
+ void snapToGrid();
+ SP<SColumnData> snapToProjectedOffset(double projectedNormalizedOffset);
+ void focusColumn(SP<SColumnData> column);
+ SP<SColumnData> getColumnAtViewportCenter();
+ SP<SColumnData> currentColumn();
+
+ double primaryViewportSize();
+ double normalizedTapeOffset();
+
CBox usableArea() const;
SP<SScrollingTargetData> dataFor(SP<ITarget> t) const;
diff --git a/src/managers/input/trackpad/gestures/ScrollMoveGesture.cpp b/src/managers/input/trackpad/gestures/ScrollMoveGesture.cpp
new file mode 100644
index 00000000..032bf679
--- /dev/null
+++ b/src/managers/input/trackpad/gestures/ScrollMoveGesture.cpp
@@ -0,0 +1,145 @@
+#include "ScrollMoveGesture.hpp"
+
+#include "../../../../desktop/state/FocusState.hpp"
+#include "../../../../desktop/Workspace.hpp"
+#include "../../../../helpers/Monitor.hpp"
+#include "../../../../layout/LayoutManager.hpp"
+#include "../../../../layout/algorithm/Algorithm.hpp"
+#include "../../../../layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp"
+#include "../../../../layout/space/Space.hpp"
+#include "../../../../config/ConfigValue.hpp"
+#include "../../../../Compositor.hpp"
+
+#include <algorithm>
+#include <cmath>
+
+constexpr double SCROLL_GESTURE_VELOCITY_DECAY = 4.0;
+constexpr double SCROLL_GESTURE_MAX_PROJECTION = 2.0;
+constexpr double SCROLL_GESTURE_VELOCITY_SMOOTH = 0.35;
+
+static Layout::Tiled::CScrollingAlgorithm* currentScrollingLayout() {
+ const auto PMONITOR = Desktop::focusState()->monitor();
+ if (!PMONITOR)
+ return nullptr;
+
+ const auto PWORKSPACE = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace;
+ if (!PWORKSPACE || !PWORKSPACE->m_space)
+ return nullptr;
+
+ const auto ALGORITHM = PWORKSPACE->m_space->algorithm();
+ if (!ALGORITHM || !ALGORITHM->tiledAlgo())
+ return nullptr;
+
+ return dynamic_cast<Layout::Tiled::CScrollingAlgorithm*>(ALGORITHM->tiledAlgo().get());
+}
+
+static float deltaForUpdate(const ITrackpadGesture::STrackpadGestureUpdate& e) {
+ if (!e.swipe)
+ return 0.F;
+
+ switch (e.direction) {
+ case TRACKPAD_GESTURE_DIR_LEFT:
+ case TRACKPAD_GESTURE_DIR_RIGHT:
+ case TRACKPAD_GESTURE_DIR_HORIZONTAL: return e.swipe->delta.x * e.scale;
+ case TRACKPAD_GESTURE_DIR_UP:
+ case TRACKPAD_GESTURE_DIR_DOWN:
+ case TRACKPAD_GESTURE_DIR_VERTICAL: return e.swipe->delta.y * e.scale;
+ default: return std::abs(e.swipe->delta.x) > std::abs(e.swipe->delta.y) ? e.swipe->delta.x * e.scale : e.swipe->delta.y * e.scale;
+ }
+}
+
+void CScrollMoveTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) {
+ ITrackpadGesture::begin(e);
+
+ const auto LAYOUT = currentScrollingLayout();
+
+ m_wasScrollingLayout = !!LAYOUT;
+ m_hasLastUpdate = false;
+ m_lastUpdateTimeMs = 0;
+ m_velocity = 0.0;
+
+ m_startedOffset = LAYOUT ? LAYOUT->normalizedTapeOffset() : 0.0;
+ m_startedColumn = LAYOUT ? LAYOUT->currentColumn() : nullptr;
+}
+
+void CScrollMoveTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) {
+ if (!m_wasScrollingLayout)
+ return;
+
+ const auto SCROLLING = currentScrollingLayout();
+ if (!SCROLLING)
+ return;
+
+ const float DELTA = deltaForUpdate(e);
+ const double PRIMARY = SCROLLING->primaryViewportSize();
+
+ if (DELTA == 0.F || PRIMARY <= 0.0)
+ return;
+
+ const double NORMALIZED_DELTA = DELTA / PRIMARY;
+ const double NORMALIZED_OFFSET_DELTA = -NORMALIZED_DELTA;
+
+ SCROLLING->moveTapeNormalized(NORMALIZED_DELTA);
+
+ if (!e.swipe)
+ return;
+
+ if (m_hasLastUpdate && e.swipe->timeMs > m_lastUpdateTimeMs) {
+ const double DT = (e.swipe->timeMs - m_lastUpdateTimeMs) / 1000.0;
+
+ if (DT > 0.0) {
+ const double INSTANT_VELOCITY = NORMALIZED_OFFSET_DELTA / DT;
+
+ if (std::isfinite(INSTANT_VELOCITY))
+ m_velocity = m_velocity * (1.0 - SCROLL_GESTURE_VELOCITY_SMOOTH) + INSTANT_VELOCITY * SCROLL_GESTURE_VELOCITY_SMOOTH;
+ }
+ }
+
+ m_hasLastUpdate = true;
+ m_lastUpdateTimeMs = e.swipe->timeMs;
+}
+
+void CScrollMoveTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) {
+ static auto PSNAP = CConfigValue<Config::BOOL>("gestures:scrolling:move_snap_to_grid");
+ static auto PSNAPCURSOR = CConfigValue<Config::BOOL>("gestures:scrolling:move_snap_cursor");
+
+ const auto SCROLLING = currentScrollingLayout();
+ if (!m_wasScrollingLayout || !SCROLLING) {
+ m_wasScrollingLayout = false;
+ m_hasLastUpdate = false;
+ m_velocity = 0.0;
+ return;
+ }
+
+ const auto CURRENT_FOCUS = Desktop::focusState()->window();
+
+ const bool CANCELLED = e.swipe && e.swipe->cancelled;
+ const double Δ = (CANCELLED ? 0.0 : std::clamp(m_velocity / SCROLL_GESTURE_VELOCITY_DECAY, -SCROLL_GESTURE_MAX_PROJECTION, SCROLL_GESTURE_MAX_PROJECTION));
+ const double PROJECTED = SCROLLING->normalizedTapeOffset() + Δ;
+
+ if (*PSNAP) {
+ const auto LANDED = SCROLLING->snapToProjectedOffset(PROJECTED);
+
+ // if we land on the same thing we started, move the tape back
+ if (LANDED == m_startedColumn && LANDED)
+ SCROLLING->moveTapeNormalized(SCROLLING->normalizedTapeOffset() - m_startedOffset);
+ else
+ SCROLLING->focusColumn(LANDED);
+ } else {
+ SCROLLING->moveTape(Δ);
+ const auto COL = SCROLLING->getColumnAtViewportCenter();
+ if (!COL->targetDatas.empty() && COL->targetDatas.front()->target) {
+ if (const auto W = COL->targetDatas.front()->target->window(); W)
+ Desktop::focusState()->fullWindowFocus(W, Desktop::FOCUS_REASON_FFM);
+ }
+ }
+
+ const auto NEW_FOCUS = Desktop::focusState()->window();
+
+ if (*PSNAPCURSOR && CURRENT_FOCUS != NEW_FOCUS && NEW_FOCUS)
+ g_pCompositor->warpCursorTo(NEW_FOCUS->middle());
+
+ m_wasScrollingLayout = false;
+ m_hasLastUpdate = false;
+ m_velocity = 0.0;
+}
diff --git a/src/managers/input/trackpad/gestures/ScrollMoveGesture.hpp b/src/managers/input/trackpad/gestures/ScrollMoveGesture.hpp
new file mode 100644
index 00000000..26e51e00
--- /dev/null
+++ b/src/managers/input/trackpad/gestures/ScrollMoveGesture.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "ITrackpadGesture.hpp"
+
+#include <cstdint>
+
+namespace Layout::Tiled {
+ struct SColumnData;
+}
+
+class CScrollMoveTrackpadGesture : public ITrackpadGesture {
+ public:
+ CScrollMoveTrackpadGesture() = default;
+ virtual ~CScrollMoveTrackpadGesture() = default;
+
+ virtual void begin(const ITrackpadGesture::STrackpadGestureBegin& e);
+ virtual void update(const ITrackpadGesture::STrackpadGestureUpdate& e);
+ virtual void end(const ITrackpadGesture::STrackpadGestureEnd& e);
+
+ private:
+ bool m_wasScrollingLayout = false;
+ bool m_hasLastUpdate = false;
+ uint32_t m_lastUpdateTimeMs = 0;
+ double m_velocity = 0.0;
+ double m_startedOffset = 0.0;
+ WP<Layout::Tiled::SColumnData> m_startedColumn = nullptr;
+};