Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/user/keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ the newly exposed columns without requiring additional pointer motion.
Bind `layout-scroll-drag` to a modified mouse button to pan a scrolling
workspace directly. The strip follows the pointer along its configured axis,
including overscroll and the same release settling used by the three-finger
gesture:
gesture. In the overview, the same bind pans the scrolling workspace row under
the pointer. Bare middle-button horizontal dragging provides the same overview
pan, while bare vertical dragging changes workspace rows:

```toml
"Mod+MouseMiddle" = "layout-scroll-drag"
Expand Down
8 changes: 5 additions & 3 deletions docs/user/workspaces-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ characters. Letter uniqueness ignores case, while badges preserve the case
written in the configuration.

Middle-click still closes a window card, but the close is sent on button
release. Drag the middle button vertically instead to step through workspace
rows without using the keyboard; moving beyond the drag threshold suppresses
the close.
release. Drag the middle button horizontally to pan the scrolling workspace row
under the pointer, or vertically to step through workspace rows without using
the keyboard. Moving beyond the drag threshold locks to the dominant axis and
suppresses the close. A configured `layout-scroll-drag` mouse bind takes
precedence and pans the row along its configured axis.

An active client drag takes precedence. Umbriel ignores requests to open the
overview until the pointer button that initiated the drag is released.
Expand Down
11 changes: 6 additions & 5 deletions src/input/cursor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,8 @@ namespace umbriel {
}
}
if (bound.has_value()) {
if (bound->action == KeybindAction::LayoutScrollDrag && m_server->gestures()->beginPointerScroll()) {
if (bound->action == KeybindAction::LayoutScrollDrag
&& m_server->gestures()->beginPointerScroll(m_cursor->x, m_cursor->y)) {
setActiveConstraint(nullptr);
m_grab = ScrollDragGrab{
.button = button,
Expand Down Expand Up @@ -828,7 +829,7 @@ namespace umbriel {
}
return;
}
overview->handleButton(button, pressed, m_cursor->x, m_cursor->y);
overview->handleButton(button, pressed, m_cursor->x, m_cursor->y, timeMsec);
return;
}

Expand Down Expand Up @@ -1086,8 +1087,8 @@ namespace umbriel {
if (Overview* overview = m_server->overview();
overview != nullptr && overview->active() && !m_server->sessionLocked() && !overviewPassthroughLayer(layer)) {
if (overview->interactive()) {
overview->handleButton(BTN_LEFT, true, lx, ly);
overview->handleButton(BTN_LEFT, false, lx, ly);
overview->handleButton(BTN_LEFT, true, lx, ly, event->time_msec);
overview->handleButton(BTN_LEFT, false, lx, ly, event->time_msec);
}
return;
}
Expand Down Expand Up @@ -1186,7 +1187,7 @@ namespace umbriel {
&& overview->active()
&& !m_server->sessionLocked()
&& m_server->seat()->wlr()->drag == nullptr) {
overview->handleMotion(m_cursor->x, m_cursor->y);
overview->handleMotion(m_cursor->x, m_cursor->y, timeMsec);
wlr_seat* seat = m_server->seat()->wlr();
if (overview->dragging()) {
wlr_seat_pointer_clear_focus(seat);
Expand Down
20 changes: 15 additions & 5 deletions src/input/gestures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ namespace umbriel {
return;
}
WorkspaceGroup* group = m_output != nullptr ? m_output->workspaceGroup() : nullptr;
if (group == nullptr || group->active() != m_scrollWorkspace) {
const bool overviewPointer = m_scrollSource == ScrollSource::OverviewPointer;
if (group == nullptr || (!overviewPointer && group->active() != m_scrollWorkspace)) {
finishScroll(true, timeMsec);
return;
}
Expand All @@ -195,15 +196,22 @@ namespace umbriel {
m_scrollWorkspace->markArrange(false);
}

bool Gestures::beginPointerScroll() {
bool Gestures::beginPointerScroll(double lx, double ly) {
if (m_server->sessionLocked()) {
return false;
}
if (m_state != State::Idle) {
cancelActive();
}
if (Overview* overview = m_server->overview(); overview != nullptr && overview->active()) {
return false;
Workspace* workspace = overview->pointerScrollWorkspace(lx, ly);
Output* output = workspace != nullptr && workspace->group() != nullptr ? workspace->group()->output() : nullptr;
m_output = output;
if (output == nullptr || !beginScroll(workspace, 1.0 / overview->contentScale(), ScrollSource::OverviewPointer)) {
m_output = nullptr;
return false;
}
return true;
}
Output* output = m_server->outputFromWlr(m_server->preferredOutput());
Workspace* workspace =
Expand All @@ -217,13 +225,15 @@ namespace umbriel {
}

void Gestures::updatePointerScroll(double dx, double dy, uint32_t timeMsec) {
if (m_state == State::Scroll && m_scrollSource == ScrollSource::Pointer) {
if (m_state == State::Scroll
&& (m_scrollSource == ScrollSource::Pointer || m_scrollSource == ScrollSource::OverviewPointer)) {
updateScroll(m_scrollVertical ? dy : dx, timeMsec);
}
}

void Gestures::endPointerScroll(bool cancelled, uint32_t timeMsec) {
if (m_state == State::Scroll && m_scrollSource == ScrollSource::Pointer) {
if (m_state == State::Scroll
&& (m_scrollSource == ScrollSource::Pointer || m_scrollSource == ScrollSource::OverviewPointer)) {
finishScroll(cancelled, timeMsec);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/input/gestures.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ namespace umbriel {
// Mouse-button bindings use the same overscroll, velocity projection, and
// column settling as the three-finger strip gesture, but pointer travel is
// mapped one-to-one to content travel.
[[nodiscard]] bool beginPointerScroll();
[[nodiscard]] bool beginPointerScroll(double lx, double ly);
void updatePointerScroll(double dx, double dy, uint32_t timeMsec);
void endPointerScroll(bool cancelled, uint32_t timeMsec);

private:
enum class State { Idle, Forward, Pending, Scroll, Switch, Overview, OverviewSelect };
enum class ScrollSource { None, Swipe, Pointer };
enum class ScrollSource { None, Swipe, Pointer, OverviewPointer };

static void onSwipeBegin(wl_listener* listener, void* data);
static void onSwipeUpdate(wl_listener* listener, void* data);
Expand Down
39 changes: 37 additions & 2 deletions src/overview/overview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "config/config.h"
#include "core/log.h"
#include "input/cursor.h"
#include "input/gestures.h"
#include "input/seat.h"
#include "layout/drop_target.h"
#include "layout/layout.h"
Expand Down Expand Up @@ -1756,19 +1757,24 @@ namespace umbriel {
}

void Overview::clearMiddlePress() {
if (m_middleScrolling) {
m_server->gestures()->endPointerScroll(true, 0);
}
if (m_middleDragging) {
m_server->cursor()->overrideCursor(nullptr);
}
m_middlePressCard = nullptr;
m_middleOutput = nullptr;
m_middlePressed = false;
m_middleDragging = false;
m_middleHorizontal = false;
m_middleScrolling = false;
m_middleAccumY = 0;
}

// -: input

bool Overview::handleButton(uint32_t button, bool pressed, double lx, double ly) {
bool Overview::handleButton(uint32_t button, bool pressed, double lx, double ly, uint32_t timeMsec) {
if (!interactive()) {
return true; // Swallow everything while zooming back in.
}
Expand All @@ -1777,6 +1783,10 @@ namespace umbriel {
if (button == BTN_MIDDLE) {
Card* card = m_middlePressCard;
const bool closeCard = m_middlePressed && !m_middleDragging;
if (m_middleScrolling) {
m_server->gestures()->endPointerScroll(false, timeMsec);
m_middleScrolling = false;
}
clearMiddlePress();
if (closeCard && card != nullptr && card->view != nullptr && card->view->mapped()) {
wlr_xdg_toplevel_send_close(card->view->toplevel());
Expand Down Expand Up @@ -1813,6 +1823,8 @@ namespace umbriel {
m_middleAccumY = 0;
m_middlePressed = true;
m_middleDragging = false;
m_middleHorizontal = false;
m_middleScrolling = false;
return true;
}
if (button != BTN_LEFT) {
Expand All @@ -1829,7 +1841,7 @@ namespace umbriel {
return true;
}

void Overview::handleMotion(double lx, double ly) {
void Overview::handleMotion(double lx, double ly, uint32_t timeMsec) {
if (!interactive()) {
return;
}
Expand All @@ -1841,10 +1853,26 @@ namespace umbriel {
return;
}
m_middleDragging = true;
m_middleHorizontal = std::abs(dx) > std::abs(dy);
if (m_middleHorizontal) {
m_middleScrolling = m_server->gestures()->beginPointerScroll(m_middlePressX, m_middlePressY);
if (m_middleScrolling) {
m_server->gestures()->updatePointerScroll(dx, dy, timeMsec);
}
}
m_middleAccumY = 0;
m_middlePressX = lx;
m_middlePressY = ly;
m_server->cursor()->overrideCursor("grabbing");
}
if (m_middleHorizontal) {
if (m_middleScrolling) {
m_server->gestures()->updatePointerScroll(lx - m_middlePressX, ly - m_middlePressY, timeMsec);
}
m_middlePressX = lx;
m_middlePressY = ly;
return;
}
m_middleAccumY += ly - m_middlePressY;
m_middlePressY = ly;
while (m_middleAccumY <= -kMiddleScrollStepPx) {
Expand Down Expand Up @@ -1906,6 +1934,13 @@ namespace umbriel {
return true;
}

Workspace* Overview::pointerScrollWorkspace(double lx, double ly) {
if (!interactive()) {
return nullptr;
}
return rowAt(lx, ly, nullptr, nullptr, true);
}

bool Overview::focusAdjacent(int direction) {
if (!interactive()) {
return false;
Expand Down
11 changes: 9 additions & 2 deletions src/overview/overview.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ namespace umbriel {
void onOutputRemoved(Output* output);

// Input entry points; called from Cursor/Keyboard while active.
bool handleButton(uint32_t button, bool pressed, double lx, double ly);
void handleMotion(double lx, double ly);
bool handleButton(uint32_t button, bool pressed, double lx, double ly, uint32_t timeMsec);
void handleMotion(double lx, double ly, uint32_t timeMsec);
bool handleAxisNotch(bool vertical, double direction, double lx, double ly);
bool handleFallbackKey(uint32_t keysym);
// Focus a neighboring column while keeping the overview card strip in
Expand All @@ -98,6 +98,11 @@ namespace umbriel {
// up the real trees are hidden, so there is nothing to slide and switching is a discrete step rather than the
// animated transition it is outside.
bool selectRelativeWorkspace(int delta, Output* output);
// Resolve the visible workspace row under a pointer drag. Horizontal
// scrolling rows extend across the output because their cards may overhang
// the centered workspace preview.
[[nodiscard]] Workspace* pointerScrollWorkspace(double lx, double ly);
[[nodiscard]] double contentScale() const { return zoom(); }
[[nodiscard]] bool dragging() const { return m_dragCard != nullptr || m_middlePressed; }

private:
Expand Down Expand Up @@ -251,6 +256,8 @@ namespace umbriel {
double m_middleAccumY = 0;
bool m_middlePressed = false;
bool m_middleDragging = false;
bool m_middleHorizontal = false;
bool m_middleScrolling = false;

Card* m_dragCard = nullptr;
double m_dragOffsetX = 0;
Expand Down
36 changes: 33 additions & 3 deletions tests/harness/checks/315_scroll_drag.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,40 @@ fi
"$UMBRIEL" msg column-focus-first > /dev/null
wait_for_a_visible x > /dev/null

# The same physical button needs no modifier in overview. The first motion crosses the drag threshold; the second
# crosses one row step. Releasing after motion must not close the card under the original press.
# The configured modified drag pans the scrolling row under the pointer in the overview too. Overview cards are scaled,
# so the gesture maps pointer travel back through that scale before updating the workspace viewport.
pointer move 560 360
"$UMBRIEL" msg overview-open > /dev/null
overview_before_x=$("$UMBRIEL" windows --json | jq -r '.[] | select(.title == "A") | .x')
pointer mod logo press "$BTN_MIDDLE" move 510 360 move 310 360 release "$BTN_MIDDLE" mod none
overview_after_x=$overview_before_x
for _ in $(seq 20); do
overview_after_x=$("$UMBRIEL" windows --json | jq -r '.[] | select(.title == "A") | .x')
((overview_after_x < overview_before_x)) && break
sleep 0.1
done
if ((overview_after_x >= overview_before_x)); then
echo "layout-scroll-drag did not pan the overview row: A x $overview_before_x -> $overview_after_x"
exit 1
fi

# Bare horizontal middle drag pans the same row without requiring the configured modifier.
bare_before_x=$overview_after_x
pointer press "$BTN_MIDDLE" move 360 360 move 560 360 release "$BTN_MIDDLE"
bare_after_x=$bare_before_x
for _ in $(seq 20); do
bare_after_x=$("$UMBRIEL" windows --json | jq -r '.[] | select(.title == "A") | .x')
((bare_after_x > bare_before_x)) && break
sleep 0.1
done
if ((bare_after_x <= bare_before_x)); then
echo "bare middle drag did not pan the overview row: A x $bare_before_x -> $bare_after_x"
exit 1
fi

# Bare vertical middle drag retains the overview's row navigation. The first motion crosses the drag threshold; the
# second crosses one row step. Releasing after motion must not close the card under the original press.
pointer move 560 360
pointer press "$BTN_MIDDLE" move 560 330 move 560 150 release "$BTN_MIDDLE"

for _ in $(seq 20); do
Expand Down Expand Up @@ -168,4 +198,4 @@ fi
wait "$middle_click_pid"
wait_for_count 3

echo "mouse drag pans both layout axes, navigates overview rows, and preserves release-only middle-click close"
echo "bound and bare mouse drags pan overview strips, navigate rows, and preserve release-only middle-click close"