Hyprland/src/layout/supplementary/DragController.cpp
Vaxry 723870337f
layout: rethonk layouts from the ground up (#12890)
Rewrites layouts to be much smaller, and deal with much less annoying
BS. Improves the overall architecture, unifies handling of pseudotiling,
and various other improvements.
2026-02-21 21:30:39 +00:00

396 lines
16 KiB
C++

#include "DragController.hpp"
#include "../space/Space.hpp"
#include "../../Compositor.hpp"
#include "../../managers/cursor/CursorShapeOverrideController.hpp"
#include "../../desktop/state/FocusState.hpp"
#include "../../desktop/view/Group.hpp"
#include "../../render/Renderer.hpp"
using namespace Layout;
using namespace Layout::Supplementary;
SP<ITarget> CDragStateController::target() const {
return m_target.lock();
}
eMouseBindMode CDragStateController::mode() const {
return m_dragMode;
}
bool CDragStateController::wasDraggingWindow() const {
return m_wasDraggingWindow;
}
bool CDragStateController::dragThresholdReached() const {
return m_dragThresholdReached;
}
void CDragStateController::resetDragThresholdReached() {
m_dragThresholdReached = false;
}
bool CDragStateController::draggingTiled() const {
return m_draggingTiled;
}
bool CDragStateController::updateDragWindow() {
const auto DRAGGINGTARGET = m_target.lock();
const bool WAS_FULLSCREEN = DRAGGINGTARGET->fullscreenMode() != FSMODE_NONE;
if (m_dragThresholdReached) {
if (WAS_FULLSCREEN) {
Log::logger->log(Log::DEBUG, "Dragging a fullscreen window");
g_pCompositor->setWindowFullscreenInternal(DRAGGINGTARGET->window(), FSMODE_NONE);
}
const auto PWORKSPACE = DRAGGINGTARGET->workspace();
const auto DRAGGINGWINDOW = DRAGGINGTARGET->window();
if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || (!DRAGGINGWINDOW->m_createdOverFullscreen && !DRAGGINGWINDOW->m_pinned))) {
Log::logger->log(Log::DEBUG, "Rejecting drag on a fullscreen workspace. (window under fullscreen)");
CKeybindManager::changeMouseBindMode(MBIND_INVALID);
return true;
}
}
m_draggingTiled = false;
m_draggingWindowOriginalFloatSize = DRAGGINGTARGET->lastFloatingSize();
if (WAS_FULLSCREEN && DRAGGINGTARGET->floating()) {
const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal();
DRAGGINGTARGET->setPositionGlobal(CBox{MOUSECOORDS - DRAGGINGTARGET->position().size() / 2.F, DRAGGINGTARGET->position().size()});
} else if (!DRAGGINGTARGET->floating() && m_dragMode == MBIND_MOVE) {
Vector2D MINSIZE = DRAGGINGTARGET->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE});
DRAGGINGTARGET->rememberFloatingSize((DRAGGINGTARGET->position().size() * 0.8489).clamp(MINSIZE, Vector2D{}).floor());
DRAGGINGTARGET->setPositionGlobal(CBox{g_pInputManager->getMouseCoordsInternal() - DRAGGINGTARGET->position().size() / 2.F, DRAGGINGTARGET->position().size()});
if (m_dragThresholdReached) {
g_layoutManager->changeFloatingMode(DRAGGINGTARGET);
m_draggingTiled = true;
}
}
const auto DRAG_ORIGINAL_BOX = DRAGGINGTARGET->position();
m_beginDragXY = g_pInputManager->getMouseCoordsInternal();
m_beginDragPositionXY = DRAG_ORIGINAL_BOX.pos();
m_beginDragSizeXY = DRAG_ORIGINAL_BOX.size();
m_lastDragXY = m_beginDragXY;
return false;
}
void CDragStateController::dragBegin(SP<ITarget> target, eMouseBindMode mode) {
m_target = target;
m_dragMode = mode;
const auto DRAGGINGTARGET = m_target.lock();
static auto PDRAGTHRESHOLD = CConfigValue<Hyprlang::INT>("binds:drag_threshold");
m_mouseMoveEventCount = 1;
m_beginDragSizeXY = Vector2D();
// Window will be floating. Let's check if it's valid. It should be, but I don't like crashing.
if (!validMapped(DRAGGINGTARGET->window())) {
Log::logger->log(Log::ERR, "Dragging attempted on an invalid window (not mapped)");
CKeybindManager::changeMouseBindMode(MBIND_INVALID);
return;
}
if (!DRAGGINGTARGET->workspace()) {
Log::logger->log(Log::ERR, "Dragging attempted on an invalid window (no workspace)");
CKeybindManager::changeMouseBindMode(MBIND_INVALID);
return;
}
// Try to pick up dragged window now if drag_threshold is disabled
// or at least update dragging related variables for the cursors
m_dragThresholdReached = *PDRAGTHRESHOLD <= 0;
if (updateDragWindow())
return;
// get the grab corner
static auto RESIZECORNER = CConfigValue<Hyprlang::INT>("general:resize_corner");
if (*RESIZECORNER != 0 && *RESIZECORNER <= 4 && DRAGGINGTARGET->floating()) {
switch (*RESIZECORNER) {
case 1:
m_grabbedCorner = CORNER_TOPLEFT;
Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
break;
case 2:
m_grabbedCorner = CORNER_TOPRIGHT;
Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
break;
case 3:
m_grabbedCorner = CORNER_BOTTOMRIGHT;
Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
break;
case 4:
m_grabbedCorner = CORNER_BOTTOMLEFT;
Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
break;
}
} else if (m_beginDragXY.x < m_beginDragPositionXY.x + m_beginDragSizeXY.x / 2.F) {
if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.F) {
m_grabbedCorner = CORNER_TOPLEFT;
Cursor::overrideController->setOverride("nw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
} else {
m_grabbedCorner = CORNER_BOTTOMLEFT;
Cursor::overrideController->setOverride("sw-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
}
} else {
if (m_beginDragXY.y < m_beginDragPositionXY.y + m_beginDragSizeXY.y / 2.F) {
m_grabbedCorner = CORNER_TOPRIGHT;
Cursor::overrideController->setOverride("ne-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
} else {
m_grabbedCorner = CORNER_BOTTOMRIGHT;
Cursor::overrideController->setOverride("se-resize", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
}
}
if (m_dragMode != MBIND_RESIZE && m_dragMode != MBIND_RESIZE_FORCE_RATIO && m_dragMode != MBIND_RESIZE_BLOCK_RATIO)
Cursor::overrideController->setOverride("grabbing", Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
DRAGGINGTARGET->damageEntire();
g_pKeybindManager->shadowKeybinds();
if (DRAGGINGTARGET->window()) {
Desktop::focusState()->rawWindowFocus(DRAGGINGTARGET->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
g_pCompositor->changeWindowZOrder(DRAGGINGTARGET->window(), true);
}
}
void CDragStateController::dragEnd() {
auto draggingTarget = m_target.lock();
m_mouseMoveEventCount = 1;
if (!validMapped(draggingTarget->window())) {
if (draggingTarget->window()) {
Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
m_target.reset();
}
return;
}
Cursor::overrideController->unsetOverride(Cursor::CURSOR_OVERRIDE_SPECIAL_ACTION);
m_target.reset();
m_wasDraggingWindow = true;
if (m_dragMode == MBIND_MOVE && draggingTarget->window()) {
draggingTarget->damageEntire();
const auto DRAGGING_WINDOW = draggingTarget->window();
const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal();
PHLWINDOW pWindow =
g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGING_WINDOW);
if (pWindow) {
if (pWindow->checkInputOnDecos(INPUT_TYPE_DRAG_END, MOUSECOORDS, DRAGGING_WINDOW))
return;
const bool FLOATEDINTOTILED = !pWindow->m_isFloating && !m_draggingTiled;
static auto PDRAGINTOGROUP = CConfigValue<Hyprlang::INT>("group:drag_into_group");
if (pWindow->m_group && DRAGGING_WINDOW->canBeGroupedInto(pWindow->m_group) && *PDRAGINTOGROUP == 1 && !FLOATEDINTOTILED) {
pWindow->m_group->add(DRAGGING_WINDOW);
// fix the draggingTarget, now it's DRAGGING_WINDOW
draggingTarget = DRAGGING_WINDOW->m_target;
}
}
}
if (m_draggingTiled) {
// static auto PPRECISEMOUSE = CConfigValue<Hyprlang::INT>("dwindle:precise_mouse_move");
// FIXME: remove or rethink
// if (*PPRECISEMOUSE) {
// eDirection direction = DIRECTION_DEFAULT;
// const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal();
// const PHLWINDOW pReferenceWindow =
// g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, DRAGGINGWINDOW);
// if (pReferenceWindow && pReferenceWindow != DRAGGINGWINDOW) {
// const Vector2D draggedCenter = DRAGGINGWINDOW->m_realPosition->goal() + DRAGGINGWINDOW->m_realSize->goal() / 2.f;
// const Vector2D referenceCenter = pReferenceWindow->m_realPosition->goal() + pReferenceWindow->m_realSize->goal() / 2.f;
// const float xDiff = draggedCenter.x - referenceCenter.x;
// const float yDiff = draggedCenter.y - referenceCenter.y;
// if (fabs(xDiff) > fabs(yDiff))
// direction = xDiff < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
// else
// direction = yDiff < 0 ? DIRECTION_UP : DIRECTION_DOWN;
// }
// onWindowRemovedTiling(DRAGGINGWINDOW);
// onWindowCreatedTiling(DRAGGINGWINDOW, direction);
// } else
// make sure to check if we are floating because drag into group could make us tiled already
if (draggingTarget->floating())
g_layoutManager->changeFloatingMode(draggingTarget);
draggingTarget->rememberFloatingSize(m_draggingWindowOriginalFloatSize);
}
draggingTarget->damageEntire();
Desktop::focusState()->fullWindowFocus(draggingTarget->window(), Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
m_wasDraggingWindow = false;
m_dragMode = MBIND_INVALID;
}
void CDragStateController::mouseMove(const Vector2D& mousePos) {
if (m_target.expired())
return;
const auto DRAGGINGTARGET = m_target.lock();
static auto PDRAGTHRESHOLD = CConfigValue<Hyprlang::INT>("binds:drag_threshold");
// Window invalid or drag begin size 0,0 meaning we rejected it.
if ((!validMapped(DRAGGINGTARGET->window()) || m_beginDragSizeXY == Vector2D())) {
CKeybindManager::changeMouseBindMode(MBIND_INVALID);
return;
}
// Yoink dragged window here instead if using drag_threshold and it has been reached
if (*PDRAGTHRESHOLD > 0 && !m_dragThresholdReached) {
if ((m_beginDragXY.distanceSq(mousePos) <= std::pow(*PDRAGTHRESHOLD, 2) && m_beginDragXY == m_lastDragXY))
return;
m_dragThresholdReached = true;
if (updateDragWindow())
return;
}
static auto TIMER = std::chrono::high_resolution_clock::now(), MSTIMER = TIMER;
const auto DELTA = Vector2D(mousePos.x - m_beginDragXY.x, mousePos.y - m_beginDragXY.y);
const auto TICKDELTA = Vector2D(mousePos.x - m_lastDragXY.x, mousePos.y - m_lastDragXY.y);
static auto SNAPENABLED = CConfigValue<Hyprlang::INT>("general:snap:enabled");
const auto TIMERDELTA = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - TIMER).count();
const auto MSDELTA = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - MSTIMER).count();
const auto MSMONITOR = 1000.0 / g_pHyprRenderer->m_mostHzMonitor->m_refreshRate;
static int totalMs = 0;
bool canSkipUpdate = true;
MSTIMER = std::chrono::high_resolution_clock::now();
if (m_mouseMoveEventCount == 1)
totalMs = 0;
if (MSMONITOR > 16.0) {
totalMs += MSDELTA < MSMONITOR ? MSDELTA : std::round(totalMs * 1.0 / m_mouseMoveEventCount);
m_mouseMoveEventCount += 1;
// check if time-window is enough to skip update on 60hz monitor
canSkipUpdate = std::clamp(MSMONITOR - TIMERDELTA, 0.0, MSMONITOR) > totalMs * 1.0 / m_mouseMoveEventCount;
}
if ((abs(TICKDELTA.x) < 1.f && abs(TICKDELTA.y) < 1.f) || (TIMERDELTA < MSMONITOR && canSkipUpdate && (m_dragMode != MBIND_MOVE)))
return;
TIMER = std::chrono::high_resolution_clock::now();
m_lastDragXY = mousePos;
DRAGGINGTARGET->damageEntire();
if (m_dragMode == MBIND_MOVE) {
Vector2D newPos = m_beginDragPositionXY + DELTA;
Vector2D newSize = DRAGGINGTARGET->position().size();
if (*SNAPENABLED && !m_draggingTiled)
g_layoutManager->performSnap(newPos, newSize, DRAGGINGTARGET, MBIND_MOVE, -1, m_beginDragSizeXY);
newPos = newPos.round();
DRAGGINGTARGET->setPositionGlobal({newPos, newSize});
DRAGGINGTARGET->warpPositionSize();
} else if (m_dragMode == MBIND_RESIZE || m_dragMode == MBIND_RESIZE_FORCE_RATIO || m_dragMode == MBIND_RESIZE_BLOCK_RATIO) {
if (DRAGGINGTARGET->floating()) {
Vector2D MINSIZE = DRAGGINGTARGET->minSize().value_or(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE});
Vector2D MAXSIZE = DRAGGINGTARGET->maxSize().value_or(Math::VECTOR2D_MAX);
Vector2D newSize = m_beginDragSizeXY;
Vector2D newPos = m_beginDragPositionXY;
if (m_grabbedCorner == CORNER_BOTTOMRIGHT)
newSize = newSize + DELTA;
else if (m_grabbedCorner == CORNER_TOPLEFT)
newSize = newSize - DELTA;
else if (m_grabbedCorner == CORNER_TOPRIGHT)
newSize = newSize + Vector2D(DELTA.x, -DELTA.y);
else if (m_grabbedCorner == CORNER_BOTTOMLEFT)
newSize = newSize + Vector2D(-DELTA.x, DELTA.y);
eMouseBindMode mode = m_dragMode;
if (DRAGGINGTARGET->window() && DRAGGINGTARGET->window()->m_ruleApplicator->keepAspectRatio().valueOrDefault() && mode != MBIND_RESIZE_BLOCK_RATIO)
mode = MBIND_RESIZE_FORCE_RATIO;
if (m_beginDragSizeXY.x >= 1 && m_beginDragSizeXY.y >= 1 && mode == MBIND_RESIZE_FORCE_RATIO) {
const float RATIO = m_beginDragSizeXY.y / m_beginDragSizeXY.x;
if (MINSIZE.x * RATIO > MINSIZE.y)
MINSIZE = Vector2D(MINSIZE.x, MINSIZE.x * RATIO);
else
MINSIZE = Vector2D(MINSIZE.y / RATIO, MINSIZE.y);
if (MAXSIZE.x * RATIO < MAXSIZE.y)
MAXSIZE = Vector2D(MAXSIZE.x, MAXSIZE.x * RATIO);
else
MAXSIZE = Vector2D(MAXSIZE.y / RATIO, MAXSIZE.y);
if (newSize.x * RATIO > newSize.y)
newSize = Vector2D(newSize.x, newSize.x * RATIO);
else
newSize = Vector2D(newSize.y / RATIO, newSize.y);
}
newSize = newSize.clamp(MINSIZE, MAXSIZE);
if (m_grabbedCorner == CORNER_TOPLEFT)
newPos = newPos - newSize + m_beginDragSizeXY;
else if (m_grabbedCorner == CORNER_TOPRIGHT)
newPos = newPos + Vector2D(0.0, (m_beginDragSizeXY - newSize).y);
else if (m_grabbedCorner == CORNER_BOTTOMLEFT)
newPos = newPos + Vector2D((m_beginDragSizeXY - newSize).x, 0.0);
if (*SNAPENABLED) {
g_layoutManager->performSnap(newPos, newSize, DRAGGINGTARGET, mode, m_grabbedCorner, m_beginDragSizeXY);
newSize = newSize.clamp(MINSIZE, MAXSIZE);
}
CBox wb = {newPos, newSize};
wb.round();
DRAGGINGTARGET->setPositionGlobal(wb);
DRAGGINGTARGET->warpPositionSize();
} else {
g_layoutManager->resizeTarget(TICKDELTA, DRAGGINGTARGET, m_grabbedCorner);
DRAGGINGTARGET->warpPositionSize();
}
}
// get middle point
Vector2D middle = DRAGGINGTARGET->position().middle();
// and check its monitor
const auto PMONITOR = g_pCompositor->getMonitorFromVector(middle);
if (PMONITOR && PMONITOR->m_activeWorkspace && DRAGGINGTARGET->floating() /* If we're resaizing a tiled target, don't do this */) {
const auto WS = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace;
DRAGGINGTARGET->assignToSpace(WS->m_space);
}
DRAGGINGTARGET->damageEntire();
}