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.
This commit is contained in:
Vaxry 2026-02-21 21:30:39 +00:00 committed by GitHub
parent 51f8849e54
commit 723870337f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 8431 additions and 5527 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,110 +0,0 @@
#pragma once
#include "IHyprLayout.hpp"
#include "../desktop/DesktopTypes.hpp"
#include <list>
#include <vector>
#include <array>
#include <optional>
#include <format>
class CHyprDwindleLayout;
enum eFullscreenMode : int8_t;
struct SDwindleNodeData {
WP<SDwindleNodeData> pParent;
bool isNode = false;
PHLWINDOWREF pWindow;
std::array<WP<SDwindleNodeData>, 2> children = {};
WP<SDwindleNodeData> self;
bool splitTop = false; // for preserve_split
CBox box = {0};
WORKSPACEID workspaceID = WORKSPACE_INVALID;
float splitRatio = 1.f;
bool valid = true;
bool ignoreFullscreenChecks = false;
// For list lookup
bool operator==(const SDwindleNodeData& rhs) const {
return pWindow.lock() == rhs.pWindow.lock() && workspaceID == rhs.workspaceID && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] &&
children[1] == rhs.children[1];
}
void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false);
void applyRootBox();
CHyprDwindleLayout* layout = nullptr;
};
class CHyprDwindleLayout : public IHyprLayout {
public:
virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT);
virtual void onWindowRemovedTiling(PHLWINDOW);
virtual bool isWindowTiled(PHLWINDOW);
virtual void recalculateMonitor(const MONITORID&);
virtual void recalculateWindow(PHLWINDOW);
virtual void onBeginDragWindow();
virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr);
virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE);
virtual std::any layoutMessage(SLayoutMessageHeader, std::string);
virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW);
virtual void switchWindows(PHLWINDOW, PHLWINDOW);
virtual void moveWindowTo(PHLWINDOW, const std::string& dir, bool silent);
virtual void alterSplitRatio(PHLWINDOW, float, bool);
virtual std::string getLayoutName();
virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to);
virtual Vector2D predictSizeForNewWindowTiled();
virtual void onEnable();
virtual void onDisable();
private:
std::vector<SP<SDwindleNodeData>> m_dwindleNodesData;
struct {
bool started = false;
bool pseudo = false;
bool xExtent = false;
bool yExtent = false;
} m_pseudoDragFlags;
std::optional<Vector2D> m_overrideFocalPoint; // for onWindowCreatedTiling.
int getNodesOnWorkspace(const WORKSPACEID&);
void applyNodeDataToWindow(SP<SDwindleNodeData>, bool force = false);
void calculateWorkspace(const PHLWORKSPACE& pWorkspace);
SP<SDwindleNodeData> getNodeFromWindow(PHLWINDOW);
SP<SDwindleNodeData> getFirstNodeOnWorkspace(const WORKSPACEID&);
SP<SDwindleNodeData> getClosestNodeOnWorkspace(const WORKSPACEID&, const Vector2D&);
SP<SDwindleNodeData> getMasterNodeOnWorkspace(const WORKSPACEID&);
void toggleSplit(PHLWINDOW);
void swapSplit(PHLWINDOW);
void moveToRoot(PHLWINDOW, bool stable = true);
eDirection m_overrideDirection = DIRECTION_DEFAULT;
friend struct SDwindleNodeData;
};
template <typename CharT>
struct std::formatter<SP<SDwindleNodeData>, CharT> : std::formatter<CharT> {
template <typename FormatContext>
auto format(const SP<SDwindleNodeData>& node, FormatContext& ctx) const {
auto out = ctx.out();
if (!node)
return std::format_to(out, "[Node nullptr]");
std::format_to(out, "[Node {:x}: workspace: {}, pos: {:j2}, size: {:j2}", rc<uintptr_t>(node.get()), node->workspaceID, node->box.pos(), node->box.size());
if (!node->isNode && !node->pWindow.expired())
std::format_to(out, ", window: {:x}", node->pWindow.lock());
return std::format_to(out, "]");
}
};

File diff suppressed because it is too large Load diff

View file

@ -1,248 +0,0 @@
#pragma once
#include "../defines.hpp"
#include "../managers/input/InputManager.hpp"
#include <any>
class CGradientValueData;
struct SWindowRenderLayoutHints {
bool isBorderGradient = false;
CGradientValueData* borderGradient = nullptr;
};
struct SLayoutMessageHeader {
PHLWINDOW pWindow;
};
enum eFullscreenMode : int8_t;
enum eRectCorner : uint8_t {
CORNER_NONE = 0,
CORNER_TOPLEFT = (1 << 0),
CORNER_TOPRIGHT = (1 << 1),
CORNER_BOTTOMRIGHT = (1 << 2),
CORNER_BOTTOMLEFT = (1 << 3),
};
inline eRectCorner cornerFromBox(const CBox& box, const Vector2D& pos) {
const auto CENTER = box.middle();
if (pos.x < CENTER.x)
return pos.y < CENTER.y ? CORNER_TOPLEFT : CORNER_BOTTOMLEFT;
return pos.y < CENTER.y ? CORNER_TOPRIGHT : CORNER_BOTTOMRIGHT;
}
enum eSnapEdge : uint8_t {
SNAP_INVALID = 0,
SNAP_UP = (1 << 0),
SNAP_DOWN = (1 << 1),
SNAP_LEFT = (1 << 2),
SNAP_RIGHT = (1 << 3),
};
enum eDirection : int8_t {
DIRECTION_DEFAULT = -1,
DIRECTION_UP = 0,
DIRECTION_RIGHT,
DIRECTION_DOWN,
DIRECTION_LEFT
};
class IHyprLayout {
public:
virtual ~IHyprLayout() = default;
virtual void onEnable() = 0;
virtual void onDisable() = 0;
/*
Called when a window is created (mapped)
The layout HAS TO set the goal pos and size (anim mgr will use it)
If !animationinprogress, then the anim mgr will not apply an anim.
*/
virtual void onWindowCreated(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT);
virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT) = 0;
virtual void onWindowCreatedFloating(PHLWINDOW);
virtual bool onWindowCreatedAutoGroup(PHLWINDOW);
/*
Return tiled status
*/
virtual bool isWindowTiled(PHLWINDOW) = 0;
/*
Called when a window is removed (unmapped)
*/
virtual void onWindowRemoved(PHLWINDOW);
virtual void onWindowRemovedTiling(PHLWINDOW) = 0;
virtual void onWindowRemovedFloating(PHLWINDOW);
/*
Called when the monitor requires a layout recalculation
this usually means reserved area changes
*/
virtual void recalculateMonitor(const MONITORID&) = 0;
/*
Called when the compositor requests a window
to be recalculated, e.g. when pseudo is toggled.
*/
virtual void recalculateWindow(PHLWINDOW) = 0;
/*
Called when a window is requested to be floated
*/
virtual void changeWindowFloatingMode(PHLWINDOW);
/*
Called when a window is clicked on, beginning a drag
this might be a resize, move, whatever the layout defines it
as.
*/
virtual void onBeginDragWindow();
/*
Called when a user requests a resize of the current window by a vec
Vector2D holds pixel values
Optional pWindow for a specific window
*/
virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr) = 0;
/*
Called when a user requests a move of the current window by a vec
Vector2D holds pixel values
Optional pWindow for a specific window
*/
virtual void moveActiveWindow(const Vector2D&, PHLWINDOW pWindow = nullptr);
/*
Called when a window is ended being dragged
(mouse up)
*/
virtual void onEndDragWindow();
/*
Called whenever the mouse moves, should the layout want to
do anything with it.
Useful for dragging.
*/
virtual void onMouseMove(const Vector2D&);
/*
Called when a window / the user requests to toggle the fullscreen state of a window
The layout sets all the fullscreen flags.
It can either accept or ignore.
*/
virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE) = 0;
/*
Called when a dispatcher requests a custom message
The layout is free to ignore.
std::any is the reply. Can be empty.
*/
virtual std::any layoutMessage(SLayoutMessageHeader, std::string) = 0;
/*
Required to be handled, but may return just SWindowRenderLayoutHints()
Called when the renderer requests any special draw flags for
a specific window, e.g. border color for groups.
*/
virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW) = 0;
/*
Called when the user requests two windows to be swapped places.
The layout is free to ignore.
*/
virtual void switchWindows(PHLWINDOW, PHLWINDOW) = 0;
/*
Called when the user requests a window move in a direction.
The layout is free to ignore.
*/
virtual void moveWindowTo(PHLWINDOW, const std::string& direction, bool silent = false) = 0;
/*
Called when the user requests to change the splitratio by or to X
on a window
*/
virtual void alterSplitRatio(PHLWINDOW, float, bool exact = false) = 0;
/*
Called when something wants the current layout's name
*/
virtual std::string getLayoutName() = 0;
/*
Called for getting the next candidate for a focus
*/
virtual PHLWINDOW getNextWindowCandidate(PHLWINDOW);
/*
Internal: called when window focus changes
*/
virtual void onWindowFocusChange(PHLWINDOW);
/*
Called for replacing any data a layout has for a new window
*/
virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to) = 0;
/*
Determines if a window can be focused. If hidden this usually means the window is part of a group.
*/
virtual bool isWindowReachable(PHLWINDOW);
/*
Called before an attempt is made to focus a window.
Brings the window to the top of any groups and ensures it is not hidden.
If the window is unmapped following this call, the focus attempt will fail.
*/
virtual void bringWindowToTop(PHLWINDOW);
/*
Called via the foreign toplevel activation protocol.
Focuses a window, bringing it to the top of its group if applicable.
May be ignored.
*/
virtual void requestFocusForWindow(PHLWINDOW);
/*
Called to predict the size of a newly opened window to send it a configure.
Return 0,0 if unpredictable
*/
virtual Vector2D predictSizeForNewWindowTiled() = 0;
/*
Prefer not overriding, use predictSizeForNewWindowTiled.
*/
virtual Vector2D predictSizeForNewWindow(PHLWINDOW pWindow);
virtual Vector2D predictSizeForNewWindowFloating(PHLWINDOW pWindow);
/*
Called to try to pick up window for dragging.
Updates drag related variables and floats window if threshold reached.
Return true to reject
*/
virtual bool updateDragWindow();
/*
Triggers a window snap event
*/
virtual void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, PHLWINDOW DRAGGINGWINDOW, const eMouseBindMode MODE, const int CORNER, const Vector2D& BEGINSIZE);
/*
Fits a floating window on its monitor
*/
virtual void fitFloatingWindowOnMonitor(PHLWINDOW w, std::optional<CBox> targetBox = std::nullopt);
/*
Returns a logical box describing the work area on a workspace
(monitor size - reserved - gapsOut)
*/
virtual CBox workAreaOnWorkspace(const PHLWORKSPACE& pWorkspace);
private:
int m_mouseMoveEventCount;
Vector2D m_beginDragXY;
Vector2D m_lastDragXY;
Vector2D m_beginDragPositionXY;
Vector2D m_beginDragSizeXY;
Vector2D m_draggingWindowOriginalFloatSize;
eRectCorner m_grabbedCorner = CORNER_TOPLEFT;
PHLWINDOWREF m_lastTiledWindow;
};

View file

@ -0,0 +1,344 @@
#include "LayoutManager.hpp"
#include "space/Space.hpp"
#include "target/Target.hpp"
#include "../config/ConfigManager.hpp"
#include "../Compositor.hpp"
#include "../managers/HookSystemManager.hpp"
#include "../desktop/state/FocusState.hpp"
#include "../desktop/view/Group.hpp"
using namespace Layout;
CLayoutManager::CLayoutManager() {
static auto P = g_pHookSystem->hookDynamic("monitorLayoutChanged", [](void* hk, SCallbackInfo& info, std::any param) {
for (const auto& ws : g_pCompositor->getWorkspaces()) {
ws->m_space->recheckWorkArea();
}
});
}
void CLayoutManager::newTarget(SP<ITarget> target, SP<CSpace> space) {
// on a new target: remember desired pos for float, if available
if (const auto DESIRED_GEOM = target->desiredGeometry(); DESIRED_GEOM)
target->rememberFloatingSize(DESIRED_GEOM->size);
target->assignToSpace(space);
}
void CLayoutManager::removeTarget(SP<ITarget> target) {
target->assignToSpace(nullptr);
}
void CLayoutManager::changeFloatingMode(SP<ITarget> target) {
if (!target->space())
return;
target->space()->toggleTargetFloating(target);
}
void CLayoutManager::beginDragTarget(SP<ITarget> target, eMouseBindMode mode) {
m_dragStateController->dragBegin(target, mode);
}
void CLayoutManager::moveMouse(const Vector2D& mousePos) {
m_dragStateController->mouseMove(mousePos);
}
void CLayoutManager::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
if (target->isPseudo()) {
auto fixedΔ = Δ;
if (corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT)
fixedΔ.x = -fixedΔ.x;
if (corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT)
fixedΔ.y = -fixedΔ.y;
auto newPseudoSize = target->pseudoSize() + fixedΔ;
const auto TARGET_TILE_SIZE = target->position().size();
newPseudoSize.x = std::clamp(newPseudoSize.x, MIN_WINDOW_SIZE, TARGET_TILE_SIZE.x);
newPseudoSize.y = std::clamp(newPseudoSize.y, MIN_WINDOW_SIZE, TARGET_TILE_SIZE.y);
target->setPseudoSize(newPseudoSize);
return;
}
target->space()->resizeTarget(Δ, target, corner);
}
std::expected<void, std::string> CLayoutManager::layoutMsg(const std::string_view& sv) {
const auto MONITOR = Desktop::focusState()->monitor();
// forward to the active workspace
if (!MONITOR)
return std::unexpected("No monitor, can't find ws to target");
auto ws = MONITOR->m_activeSpecialWorkspace ? MONITOR->m_activeSpecialWorkspace : MONITOR->m_activeWorkspace;
if (!ws)
return std::unexpected("No workspace, can't target");
return ws->m_space->layoutMsg(sv);
}
void CLayoutManager::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
if (!target->floating())
return;
target->space()->moveTarget(Δ, target);
}
void CLayoutManager::endDragTarget() {
m_dragStateController->dragEnd();
}
void CLayoutManager::fullscreenRequestForTarget(SP<ITarget> target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode) {
target->space()->setFullscreen(target, effectiveMode);
}
void CLayoutManager::switchTargets(SP<ITarget> a, SP<ITarget> b, bool preserveFocus) {
if (preserveFocus) {
a->swap(b);
return;
}
const auto IS_A_ACTIVE = Desktop::focusState()->window() == a->window();
const auto IS_B_ACTIVE = Desktop::focusState()->window() == b->window();
a->swap(b);
if (IS_A_ACTIVE && b->window())
Desktop::focusState()->fullWindowFocus(b->window(), Desktop::FOCUS_REASON_KEYBIND);
if (IS_B_ACTIVE && a->window())
Desktop::focusState()->fullWindowFocus(a->window(), Desktop::FOCUS_REASON_KEYBIND);
}
void CLayoutManager::moveInDirection(SP<ITarget> target, const std::string& direction, bool silent) {
Math::eDirection dir = Math::fromChar(direction.at(0));
if (dir == Math::DIRECTION_DEFAULT) {
Log::logger->log(Log::ERR, "invalid direction for moveInDirection: {}", direction);
return;
}
target->space()->moveTargetInDirection(target, dir, silent);
}
SP<ITarget> CLayoutManager::getNextCandidate(SP<CSpace> space, SP<ITarget> from) {
return space->getNextCandidate(from);
}
bool CLayoutManager::isReachable(SP<ITarget> target) {
return true;
}
void CLayoutManager::bringTargetToTop(SP<ITarget> target) {
if (!target)
return;
if (target->window()->m_group) {
// grouped, change the current to this window
target->window()->m_group->setCurrent(target->window());
}
}
std::optional<Vector2D> CLayoutManager::predictSizeForNewTiledTarget() {
const auto FOCUSED_MON = Desktop::focusState()->monitor();
if (!FOCUSED_MON || !FOCUSED_MON->m_activeWorkspace)
return std::nullopt;
if (FOCUSED_MON->m_activeSpecialWorkspace)
return FOCUSED_MON->m_activeSpecialWorkspace->m_space->predictSizeForNewTiledTarget();
return FOCUSED_MON->m_activeWorkspace->m_space->predictSizeForNewTiledTarget();
}
const UP<Supplementary::CDragStateController>& CLayoutManager::dragController() {
return m_dragStateController;
}
static inline bool canSnap(const double SIDEA, const double SIDEB, const double GAP) {
return std::abs(SIDEA - SIDEB) < GAP;
}
static void snapMove(double& start, double& end, const double P) {
end = P + (end - start);
start = P;
}
static void snapResize(double& start, double& end, const double P) {
start = P;
}
using SnapFn = std::function<void(double&, double&, const double)>;
void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP<ITarget> DRAGGINGTARGET, const eMouseBindMode MODE, const int CORNER, const Vector2D& BEGINSIZE) {
const auto DRAGGINGWINDOW = DRAGGINGTARGET->window();
if (!Desktop::View::validMapped(DRAGGINGWINDOW))
return;
static auto SNAPWINDOWGAP = CConfigValue<Hyprlang::INT>("general:snap:window_gap");
static auto SNAPMONITORGAP = CConfigValue<Hyprlang::INT>("general:snap:monitor_gap");
static auto SNAPBORDEROVERLAP = CConfigValue<Hyprlang::INT>("general:snap:border_overlap");
static auto SNAPRESPECTGAPS = CConfigValue<Hyprlang::INT>("general:snap:respect_gaps");
static auto PGAPSIN = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_in");
static auto PGAPSOUT = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_out");
const auto GAPSNONE = CCssGapData{0, 0, 0, 0};
const SnapFn SNAP = (MODE == MBIND_MOVE) ? snapMove : snapResize;
int snaps = 0;
struct SRange {
double start = 0;
double end = 0;
};
const auto EXTENTS = DRAGGINGWINDOW->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS);
SRange sourceX = {sourcePos.x - EXTENTS.topLeft.x, sourcePos.x + sourceSize.x + EXTENTS.bottomRight.x};
SRange sourceY = {sourcePos.y - EXTENTS.topLeft.y, sourcePos.y + sourceSize.y + EXTENTS.bottomRight.y};
if (*SNAPWINDOWGAP) {
const double GAPSIZE = *SNAPWINDOWGAP;
const auto WSID = DRAGGINGWINDOW->workspaceID();
const bool HASFULLSCREEN = DRAGGINGWINDOW->m_workspace && DRAGGINGWINDOW->m_workspace->m_hasFullscreenWindow;
const auto* GAPSIN = *SNAPRESPECTGAPS ? sc<CCssGapData*>(PGAPSIN.ptr()->getData()) : &GAPSNONE;
const double GAPSX = GAPSIN->m_left + GAPSIN->m_right;
const double GAPSY = GAPSIN->m_top + GAPSIN->m_bottom;
for (auto& other : g_pCompositor->m_windows) {
if ((HASFULLSCREEN && !other->m_createdOverFullscreen) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut ||
other->isX11OverrideRedirect())
continue;
const CBox SURF = other->getWindowBoxUnified(Desktop::View::RESERVED_EXTENTS);
const SRange SURFBX = {SURF.x - GAPSX, SURF.x + SURF.w + GAPSX};
const SRange SURFBY = {SURF.y - GAPSY, SURF.y + SURF.h + GAPSY};
// only snap windows if their ranges overlap in the opposite axis
if (sourceY.start <= SURFBY.end && SURFBY.start <= sourceY.end) {
if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && canSnap(sourceX.start, SURFBX.end, GAPSIZE)) {
SNAP(sourceX.start, sourceX.end, SURFBX.end);
snaps |= SNAP_LEFT;
} else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && canSnap(sourceX.end, SURFBX.start, GAPSIZE)) {
SNAP(sourceX.end, sourceX.start, SURFBX.start);
snaps |= SNAP_RIGHT;
}
}
if (sourceX.start <= SURFBX.end && SURFBX.start <= sourceX.end) {
if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && canSnap(sourceY.start, SURFBY.end, GAPSIZE)) {
SNAP(sourceY.start, sourceY.end, SURFBY.end);
snaps |= SNAP_UP;
} else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && canSnap(sourceY.end, SURFBY.start, GAPSIZE)) {
SNAP(sourceY.end, sourceY.start, SURFBY.start);
snaps |= SNAP_DOWN;
}
}
// corner snapping
if (sourceX.start == SURFBX.end || SURFBX.start == sourceX.end) {
const SRange SURFY = {SURFBY.start + GAPSY, SURFBY.end - GAPSY};
if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && !(snaps & SNAP_UP) && canSnap(sourceY.start, SURFY.start, GAPSIZE)) {
SNAP(sourceY.start, sourceY.end, SURFY.start);
snaps |= SNAP_UP;
} else if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_DOWN) && canSnap(sourceY.end, SURFY.end, GAPSIZE)) {
SNAP(sourceY.end, sourceY.start, SURFY.end);
snaps |= SNAP_DOWN;
}
}
if (sourceY.start == SURFBY.end || SURFBY.start == sourceY.end) {
const SRange SURFX = {SURFBX.start + GAPSX, SURFBX.end - GAPSX};
if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && !(snaps & SNAP_LEFT) && canSnap(sourceX.start, SURFX.start, GAPSIZE)) {
SNAP(sourceX.start, sourceX.end, SURFX.start);
snaps |= SNAP_LEFT;
} else if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && !(snaps & SNAP_RIGHT) && canSnap(sourceX.end, SURFX.end, GAPSIZE)) {
SNAP(sourceX.end, sourceX.start, SURFX.end);
snaps |= SNAP_RIGHT;
}
}
}
}
if (*SNAPMONITORGAP) {
const double GAPSIZE = *SNAPMONITORGAP;
const auto EXTENTNONE = SBoxExtents{{0, 0}, {0, 0}};
const auto* EXTENTDIFF = *SNAPBORDEROVERLAP ? &EXTENTS : &EXTENTNONE;
const auto MON = DRAGGINGWINDOW->m_monitor.lock();
const auto* GAPSOUT = *SNAPRESPECTGAPS ? sc<CCssGapData*>(PGAPSOUT.ptr()->getData()) : &GAPSNONE;
const auto WORK_AREA = Desktop::CReservedArea{GAPSOUT->m_top, GAPSOUT->m_right, GAPSOUT->m_bottom, GAPSOUT->m_left}.apply(MON->logicalBoxMinusReserved());
SRange monX = {WORK_AREA.x, WORK_AREA.x + WORK_AREA.w};
SRange monY = {WORK_AREA.y, WORK_AREA.y + WORK_AREA.h};
const bool HAS_LEFT = MON->m_reservedArea.left() > 0;
const bool HAS_TOP = MON->m_reservedArea.top() > 0;
const bool HAS_BOTTOM = MON->m_reservedArea.bottom() > 0;
const bool HAS_RIGHT = MON->m_reservedArea.right() > 0;
if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) &&
((HAS_LEFT && canSnap(sourceX.start, monX.start, GAPSIZE)) || canSnap(sourceX.start, (monX.start -= MON->m_reservedArea.left() + EXTENTDIFF->topLeft.x), GAPSIZE))) {
SNAP(sourceX.start, sourceX.end, monX.start);
snaps |= SNAP_LEFT;
}
if (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) &&
((HAS_RIGHT && canSnap(sourceX.end, monX.end, GAPSIZE)) || canSnap(sourceX.end, (monX.end += MON->m_reservedArea.right() + EXTENTDIFF->bottomRight.x), GAPSIZE))) {
SNAP(sourceX.end, sourceX.start, monX.end);
snaps |= SNAP_RIGHT;
}
if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) &&
((HAS_TOP && canSnap(sourceY.start, monY.start, GAPSIZE)) || canSnap(sourceY.start, (monY.start -= MON->m_reservedArea.top() + EXTENTDIFF->topLeft.y), GAPSIZE))) {
SNAP(sourceY.start, sourceY.end, monY.start);
snaps |= SNAP_UP;
}
if (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) &&
((HAS_BOTTOM && canSnap(sourceY.end, monY.end, GAPSIZE)) || canSnap(sourceY.end, (monY.end += MON->m_reservedArea.bottom() + EXTENTDIFF->bottomRight.y), GAPSIZE))) {
SNAP(sourceY.end, sourceY.start, monY.end);
snaps |= SNAP_DOWN;
}
}
// remove extents from main surface
sourceX = {sourceX.start + EXTENTS.topLeft.x, sourceX.end - EXTENTS.bottomRight.x};
sourceY = {sourceY.start + EXTENTS.topLeft.y, sourceY.end - EXTENTS.bottomRight.y};
if (MODE == MBIND_RESIZE_FORCE_RATIO) {
if ((CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT) && snaps & SNAP_LEFT) || (CORNER & (CORNER_TOPRIGHT | CORNER_BOTTOMRIGHT) && snaps & SNAP_RIGHT)) {
const double SIZEY = (sourceX.end - sourceX.start) * (BEGINSIZE.y / BEGINSIZE.x);
if (CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT))
sourceY.start = sourceY.end - SIZEY;
else
sourceY.end = sourceY.start + SIZEY;
} else if ((CORNER & (CORNER_TOPLEFT | CORNER_TOPRIGHT) && snaps & SNAP_UP) || (CORNER & (CORNER_BOTTOMLEFT | CORNER_BOTTOMRIGHT) && snaps & SNAP_DOWN)) {
const double SIZEX = (sourceY.end - sourceY.start) * (BEGINSIZE.x / BEGINSIZE.y);
if (CORNER & (CORNER_TOPLEFT | CORNER_BOTTOMLEFT))
sourceX.start = sourceX.end - SIZEX;
else
sourceX.end = sourceX.start + SIZEX;
}
}
sourcePos = {sourceX.start, sourceY.start};
sourceSize = {sourceX.end - sourceX.start, sourceY.end - sourceY.start};
}
void CLayoutManager::recalculateMonitor(PHLMONITOR m) {
if (m->m_activeSpecialWorkspace)
m->m_activeSpecialWorkspace->m_space->recalculate();
if (m->m_activeWorkspace)
m->m_activeWorkspace->m_space->recalculate();
}
void CLayoutManager::invalidateMonitorGeometries(PHLMONITOR m) {
for (const auto& ws : g_pCompositor->getWorkspaces()) {
if (ws && ws->m_monitor == m) {
ws->m_space->recheckWorkArea();
ws->m_space->recalculate();
}
}
}

View file

@ -0,0 +1,86 @@
#pragma once
#include "../helpers/memory/Memory.hpp"
#include "../helpers/math/Math.hpp"
#include "../managers/input/InputManager.hpp"
#include "supplementary/DragController.hpp"
#include <optional>
#include <expected>
enum eFullscreenMode : int8_t;
namespace Layout {
class ITarget;
class CSpace;
enum eRectCorner : uint8_t {
CORNER_NONE = 0,
CORNER_TOPLEFT = (1 << 0),
CORNER_TOPRIGHT = (1 << 1),
CORNER_BOTTOMRIGHT = (1 << 2),
CORNER_BOTTOMLEFT = (1 << 3),
};
inline eRectCorner cornerFromBox(const CBox& box, const Vector2D& pos) {
const auto CENTER = box.middle();
if (pos.x < CENTER.x)
return pos.y < CENTER.y ? CORNER_TOPLEFT : CORNER_BOTTOMLEFT;
return pos.y < CENTER.y ? CORNER_TOPRIGHT : CORNER_BOTTOMRIGHT;
}
enum eSnapEdge : uint8_t {
SNAP_INVALID = 0,
SNAP_UP = (1 << 0),
SNAP_DOWN = (1 << 1),
SNAP_LEFT = (1 << 2),
SNAP_RIGHT = (1 << 3),
};
class CLayoutManager {
public:
CLayoutManager();
~CLayoutManager() = default;
void newTarget(SP<ITarget> target, SP<CSpace> space);
void removeTarget(SP<ITarget> target);
void changeFloatingMode(SP<ITarget> target);
void beginDragTarget(SP<ITarget> target, eMouseBindMode mode);
void moveMouse(const Vector2D& mousePos);
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void endDragTarget();
std::expected<void, std::string> layoutMsg(const std::string_view& sv);
void fullscreenRequestForTarget(SP<ITarget> target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode);
void switchTargets(SP<ITarget> a, SP<ITarget> b, bool preserveFocus = true);
void moveInDirection(SP<ITarget> target, const std::string& direction, bool silent = false);
SP<ITarget> getNextCandidate(SP<CSpace> space, SP<ITarget> from);
bool isReachable(SP<ITarget> target);
void bringTargetToTop(SP<ITarget> target);
std::optional<Vector2D> predictSizeForNewTiledTarget();
void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP<ITarget> target, eMouseBindMode mode, int corner, const Vector2D& beginSize);
void invalidateMonitorGeometries(PHLMONITOR);
void recalculateMonitor(PHLMONITOR);
const UP<Supplementary::CDragStateController>& dragController();
private:
UP<Supplementary::CDragStateController> m_dragStateController = makeUnique<Supplementary::CDragStateController>();
};
}
inline UP<Layout::CLayoutManager> g_layoutManager;

File diff suppressed because it is too large Load diff

View file

@ -1,112 +0,0 @@
#pragma once
#include "IHyprLayout.hpp"
#include "../desktop/DesktopTypes.hpp"
#include "../helpers/varlist/VarList.hpp"
#include <vector>
#include <list>
#include <any>
enum eFullscreenMode : int8_t;
//orientation determines which side of the screen the master area resides
enum eOrientation : uint8_t {
ORIENTATION_LEFT = 0,
ORIENTATION_TOP,
ORIENTATION_RIGHT,
ORIENTATION_BOTTOM,
ORIENTATION_CENTER
};
struct SMasterNodeData {
bool isMaster = false;
float percMaster = 0.5f;
PHLWINDOWREF pWindow;
Vector2D position;
Vector2D size;
float percSize = 1.f; // size multiplier for resizing children
WORKSPACEID workspaceID = WORKSPACE_INVALID;
bool ignoreFullscreenChecks = false;
//
bool operator==(const SMasterNodeData& rhs) const {
return pWindow.lock() == rhs.pWindow.lock();
}
};
struct SMasterWorkspaceData {
WORKSPACEID workspaceID = WORKSPACE_INVALID;
eOrientation orientation = ORIENTATION_LEFT;
// Previously focused non-master window when `focusmaster previous` command was issued
PHLWINDOWREF focusMasterPrev;
//
bool operator==(const SMasterWorkspaceData& rhs) const {
return workspaceID == rhs.workspaceID;
}
};
class CHyprMasterLayout : public IHyprLayout {
public:
virtual void onWindowCreatedTiling(PHLWINDOW, eDirection direction = DIRECTION_DEFAULT);
virtual void onWindowRemovedTiling(PHLWINDOW);
virtual bool isWindowTiled(PHLWINDOW);
virtual void recalculateMonitor(const MONITORID&);
virtual void recalculateWindow(PHLWINDOW);
virtual void resizeActiveWindow(const Vector2D&, eRectCorner corner = CORNER_NONE, PHLWINDOW pWindow = nullptr);
virtual void fullscreenRequestForWindow(PHLWINDOW pWindow, const eFullscreenMode CURRENT_EFFECTIVE_MODE, const eFullscreenMode EFFECTIVE_MODE);
virtual std::any layoutMessage(SLayoutMessageHeader, std::string);
virtual SWindowRenderLayoutHints requestRenderHints(PHLWINDOW);
virtual void switchWindows(PHLWINDOW, PHLWINDOW);
virtual void moveWindowTo(PHLWINDOW, const std::string& dir, bool silent);
virtual void alterSplitRatio(PHLWINDOW, float, bool);
virtual std::string getLayoutName();
virtual void replaceWindowDataWith(PHLWINDOW from, PHLWINDOW to);
virtual Vector2D predictSizeForNewWindowTiled();
virtual void onEnable();
virtual void onDisable();
private:
std::list<SMasterNodeData> m_masterNodesData;
std::vector<SMasterWorkspaceData> m_masterWorkspacesData;
bool m_forceWarps = false;
void buildOrientationCycleVectorFromVars(std::vector<eOrientation>& cycle, CVarList& vars);
void buildOrientationCycleVectorFromEOperation(std::vector<eOrientation>& cycle);
void runOrientationCycle(SLayoutMessageHeader& header, CVarList* vars, int next);
eOrientation getDynamicOrientation(PHLWORKSPACE);
int getNodesOnWorkspace(const WORKSPACEID&);
void applyNodeDataToWindow(SMasterNodeData*);
SMasterNodeData* getNodeFromWindow(PHLWINDOW);
SMasterNodeData* getMasterNodeOnWorkspace(const WORKSPACEID&);
SMasterWorkspaceData* getMasterWorkspaceData(const WORKSPACEID&);
void calculateWorkspace(PHLWORKSPACE);
PHLWINDOW getNextWindow(PHLWINDOW, bool, bool);
int getMastersOnWorkspace(const WORKSPACEID&);
friend struct SMasterNodeData;
friend struct SMasterWorkspaceData;
};
template <typename CharT>
struct std::formatter<SMasterNodeData*, CharT> : std::formatter<CharT> {
template <typename FormatContext>
auto format(const SMasterNodeData* const& node, FormatContext& ctx) const {
auto out = ctx.out();
if (!node)
return std::format_to(out, "[Node nullptr]");
std::format_to(out, "[Node {:x}: workspace: {}, pos: {:j2}, size: {:j2}", rc<uintptr_t>(node), node->workspaceID, node->position, node->size);
if (node->isMaster)
std::format_to(out, ", master");
if (!node->pWindow.expired())
std::format_to(out, ", window: {:x}", node->pWindow.lock());
return std::format_to(out, "]");
}
};

View file

@ -0,0 +1,264 @@
#include "Algorithm.hpp"
#include "FloatingAlgorithm.hpp"
#include "TiledAlgorithm.hpp"
#include "../target/WindowTarget.hpp"
#include "../space/Space.hpp"
#include "../../desktop/view/Window.hpp"
#include "../../desktop/history/WindowHistoryTracker.hpp"
#include "../../helpers/Monitor.hpp"
#include "../../render/Renderer.hpp"
#include "../../debug/log/Logger.hpp"
using namespace Layout;
SP<CAlgorithm> CAlgorithm::create(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space) {
auto algo = SP<CAlgorithm>(new CAlgorithm(std::move(tiled), std::move(floating), space));
algo->m_self = algo;
algo->m_tiled->m_parent = algo;
algo->m_floating->m_parent = algo;
return algo;
}
CAlgorithm::CAlgorithm(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space) :
m_tiled(std::move(tiled)), m_floating(std::move(floating)), m_space(space) {
;
}
void CAlgorithm::addTarget(SP<ITarget> target) {
const bool SHOULD_FLOAT = target->floating();
if (SHOULD_FLOAT) {
m_floatingTargets.emplace_back(target);
m_floating->newTarget(target);
} else {
m_tiledTargets.emplace_back(target);
m_tiled->newTarget(target);
}
}
void CAlgorithm::removeTarget(SP<ITarget> target) {
const bool IS_FLOATING = std::ranges::contains(m_floatingTargets, target);
if (IS_FLOATING) {
m_floating->removeTarget(target);
std::erase(m_floatingTargets, target);
return;
}
const bool IS_TILED = std::ranges::contains(m_tiledTargets, target);
if (IS_TILED) {
m_tiled->removeTarget(target);
std::erase(m_tiledTargets, target);
return;
}
Log::logger->log(Log::ERR, "BUG THIS: CAlgorithm::removeTarget, but not found");
}
void CAlgorithm::moveTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint, bool reposition) {
const bool SHOULD_FLOAT = target->floating();
if (SHOULD_FLOAT) {
m_floatingTargets.emplace_back(target);
if (reposition)
m_floating->newTarget(target);
else
m_floating->movedTarget(target, focalPoint);
} else {
m_tiledTargets.emplace_back(target);
if (reposition)
m_tiled->newTarget(target);
else
m_tiled->movedTarget(target, focalPoint);
}
}
SP<CSpace> CAlgorithm::space() const {
return m_space.lock();
}
void CAlgorithm::setFloating(SP<ITarget> target, bool floating, bool reposition) {
removeTarget(target);
g_pHyprRenderer->damageWindow(target->window());
target->setFloating(floating);
moveTarget(target, std::nullopt, reposition);
g_pHyprRenderer->damageWindow(target->window());
}
size_t CAlgorithm::tiledTargets() const {
return m_tiledTargets.size();
}
size_t CAlgorithm::floatingTargets() const {
return m_floatingTargets.size();
}
void CAlgorithm::recalculate() {
m_tiled->recalculate();
m_floating->recalculate();
const auto PWORKSPACE = m_space->workspace();
const auto PMONITOR = PWORKSPACE->m_monitor;
if (PWORKSPACE->m_hasFullscreenWindow && PMONITOR) {
// massive hack from the fullscreen func
const auto PFULLWINDOW = PWORKSPACE->getFullscreenWindow();
if (PFULLWINDOW) {
if (PWORKSPACE->m_fullscreenMode == FSMODE_FULLSCREEN) {
*PFULLWINDOW->m_realPosition = PMONITOR->m_position;
*PFULLWINDOW->m_realSize = PMONITOR->m_size;
} else if (PWORKSPACE->m_fullscreenMode == FSMODE_MAXIMIZED)
PFULLWINDOW->layoutTarget()->setPositionGlobal(m_space->workArea());
}
return;
}
}
void CAlgorithm::recenter(SP<ITarget> t) {
if (t->floating())
m_floating->recenter(t);
}
std::expected<void, std::string> CAlgorithm::layoutMsg(const std::string_view& sv) {
if (const auto ret = m_floating->layoutMsg(sv); !ret)
return ret;
return m_tiled->layoutMsg(sv);
}
std::optional<Vector2D> CAlgorithm::predictSizeForNewTiledTarget() {
return m_tiled->predictSizeForNewTarget();
}
void CAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
if (target->floating())
m_floating->resizeTarget(Δ, target, corner);
else
m_tiled->resizeTarget(Δ, target, corner);
}
void CAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
if (target->floating())
m_floating->moveTarget(Δ, target);
}
void CAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto swapFirst = [&a, &b](std::vector<WP<ITarget>>& targets) -> bool {
auto ia = std::ranges::find(targets, a);
auto ib = std::ranges::find(targets, b);
if (ia != std::ranges::end(targets) && ib != std::ranges::end(targets)) {
std::iter_swap(ia, ib);
return true;
} else if (ia != std::ranges::end(targets))
*ia = b;
else if (ib != std::ranges::end(targets))
*ib = a;
return false;
};
if (!swapFirst(m_tiledTargets))
swapFirst(m_floatingTargets);
const WP<IModeAlgorithm> algA = a->floating() ? WP<IModeAlgorithm>(m_floating) : WP<IModeAlgorithm>(m_tiled);
const WP<IModeAlgorithm> algB = b->floating() ? WP<IModeAlgorithm>(m_floating) : WP<IModeAlgorithm>(m_tiled);
algA->swapTargets(a, b);
if (algA != algB)
algB->swapTargets(b, a);
}
void CAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
if (t->floating())
m_floating->moveTargetInDirection(t, dir, silent);
else
m_tiled->moveTargetInDirection(t, dir, silent);
}
void CAlgorithm::updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo) {
algo->m_parent = m_self;
for (const auto& t : m_floatingTargets) {
const auto TARGET = t.lock();
if (!TARGET)
continue;
// Unhide windows when switching layouts to prevent them from being permanently lost
const auto WINDOW = TARGET->window();
if (WINDOW)
WINDOW->setHidden(false);
m_floating->removeTarget(TARGET);
algo->newTarget(TARGET);
}
m_floating = std::move(algo);
}
void CAlgorithm::updateTiledAlgo(UP<ITiledAlgorithm>&& algo) {
algo->m_parent = m_self;
for (const auto& t : m_tiledTargets) {
const auto TARGET = t.lock();
if (!TARGET)
continue;
// Unhide windows when switching layouts to prevent them from being permanently lost
// This is a safeguard for layouts (including third-party plugins) that use setHidden
const auto WINDOW = TARGET->window();
if (WINDOW)
WINDOW->setHidden(false);
m_tiled->removeTarget(TARGET);
algo->newTarget(TARGET);
}
m_tiled = std::move(algo);
}
const UP<ITiledAlgorithm>& CAlgorithm::tiledAlgo() const {
return m_tiled;
}
const UP<IFloatingAlgorithm>& CAlgorithm::floatingAlgo() const {
return m_floating;
}
SP<ITarget> CAlgorithm::getNextCandidate(SP<ITarget> old) {
if (old->floating()) {
// use window history to determine best target
for (const auto& w : Desktop::History::windowTracker()->fullHistory() | std::views::reverse) {
if (!w->m_workspace || w->m_workspace->m_space != m_space || !w->layoutTarget() || !w->layoutTarget()->space())
continue;
return w->layoutTarget();
}
// no history, fall back
} else {
// ask the layout
const auto CANDIDATE = m_tiled->getNextCandidate(old);
if (CANDIDATE)
return CANDIDATE;
// no candidate, fall back
}
// fallback: try to focus anything
if (!m_tiledTargets.empty())
return m_tiledTargets.back().lock();
if (!m_floatingTargets.empty())
return m_floatingTargets.back().lock();
// god damn it, maybe empty?
return nullptr;
}

View file

@ -0,0 +1,64 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/math/Direction.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "../LayoutManager.hpp"
#include <expected>
#include <optional>
namespace Layout {
class ITarget;
class IFloatingAlgorithm;
class ITiledAlgorithm;
class CSpace;
class CAlgorithm {
public:
static SP<CAlgorithm> create(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space);
~CAlgorithm() = default;
void addTarget(SP<ITarget> target);
void moveTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt, bool reposition = false);
void removeTarget(SP<ITarget> target);
void swapTargets(SP<ITarget> a, SP<ITarget> b);
void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
SP<ITarget> getNextCandidate(SP<ITarget> old);
void setFloating(SP<ITarget> target, bool floating, bool reposition = false);
std::expected<void, std::string> layoutMsg(const std::string_view& sv);
std::optional<Vector2D> predictSizeForNewTiledTarget();
void recalculate();
void recenter(SP<ITarget> t);
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target);
void updateFloatingAlgo(UP<IFloatingAlgorithm>&& algo);
void updateTiledAlgo(UP<ITiledAlgorithm>&& algo);
const UP<ITiledAlgorithm>& tiledAlgo() const;
const UP<IFloatingAlgorithm>& floatingAlgo() const;
SP<CSpace> space() const;
size_t tiledTargets() const;
size_t floatingTargets() const;
private:
CAlgorithm(UP<ITiledAlgorithm>&& tiled, UP<IFloatingAlgorithm>&& floating, SP<CSpace> space);
UP<ITiledAlgorithm> m_tiled;
UP<IFloatingAlgorithm> m_floating;
WP<CSpace> m_space;
WP<CAlgorithm> m_self;
std::vector<WP<ITarget>> m_tiledTargets, m_floatingTargets;
};
}

View file

@ -0,0 +1,18 @@
#include "FloatingAlgorithm.hpp"
#include "Algorithm.hpp"
#include "../space/Space.hpp"
using namespace Layout;
void IFloatingAlgorithm::recalculate() {
;
}
void IFloatingAlgorithm::recenter(SP<ITarget> t) {
const auto LAST = t->lastFloatingSize();
if (LAST.x <= 5 || LAST.y <= 5)
return;
t->setPositionGlobal({m_parent->space()->workArea().middle() - LAST / 2.F, LAST});
}

View file

@ -0,0 +1,31 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "ModeAlgorithm.hpp"
namespace Layout {
class ITarget;
class CAlgorithm;
class IFloatingAlgorithm : public IModeAlgorithm {
public:
virtual ~IFloatingAlgorithm() = default;
// a target is being moved by a delta
virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target) = 0;
virtual void recenter(SP<ITarget> t);
virtual void recalculate();
protected:
IFloatingAlgorithm() = default;
WP<CAlgorithm> m_parent;
friend class Layout::CAlgorithm;
};
}

View file

@ -0,0 +1,11 @@
#include "ModeAlgorithm.hpp"
using namespace Layout;
std::expected<void, std::string> IModeAlgorithm::layoutMsg(const std::string_view& sv) {
return {};
}
std::optional<Vector2D> IModeAlgorithm::predictSizeForNewTarget() {
return std::nullopt;
}

View file

@ -0,0 +1,54 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/math/Direction.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "../LayoutManager.hpp"
#include <expected>
namespace Layout {
class ITarget;
class CAlgorithm;
class IModeAlgorithm {
public:
virtual ~IModeAlgorithm() = default;
// a completely new target
virtual void newTarget(SP<ITarget> target) = 0;
// a target moved into the algorithm (from another)
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt) = 0;
// a target removed
virtual void removeTarget(SP<ITarget> target) = 0;
// a target is being resized by a delta. Corner none likely means not interactive
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE) = 0;
// recalculate layout
virtual void recalculate() = 0;
// swap targets
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b) = 0;
// move a target in a given direction
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) = 0;
// optional: handle layout messages
virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
// optional: predict new window's size
virtual std::optional<Vector2D> predictSizeForNewTarget();
protected:
IModeAlgorithm() = default;
WP<CAlgorithm> m_parent;
friend class Layout::CAlgorithm;
};
}

View file

@ -0,0 +1,26 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "ModeAlgorithm.hpp"
namespace Layout {
class ITarget;
class CAlgorithm;
class ITiledAlgorithm : public IModeAlgorithm {
public:
virtual ~ITiledAlgorithm() = default;
virtual SP<ITarget> getNextCandidate(SP<ITarget> old) = 0;
protected:
ITiledAlgorithm() = default;
WP<CAlgorithm> m_parent;
friend class Layout::CAlgorithm;
};
}

View file

@ -0,0 +1,223 @@
#include "DefaultFloatingAlgorithm.hpp"
#include "../../Algorithm.hpp"
#include "../../../target/WindowTarget.hpp"
#include "../../../space/Space.hpp"
#include "../../../../Compositor.hpp"
#include "../../../../helpers/Monitor.hpp"
using namespace Layout;
using namespace Layout::Floating;
constexpr const Vector2D DEFAULT_SIZE = {640, 400};
//
void CDefaultFloatingAlgorithm::newTarget(SP<ITarget> target) {
const auto WORK_AREA = m_parent->space()->workArea(true);
const auto DESIRED_GEOM = target->desiredGeometry();
const auto MONITOR_POS = m_parent->space()->workspace()->m_monitor->logicalBox().pos();
CBox windowGeometry;
if (!DESIRED_GEOM) {
switch (DESIRED_GEOM.error()) {
case GEOMETRY_INVALID_DESIRED: {
// if the desired is invalid, we hide the window.
if (target->type() == TARGET_TYPE_WINDOW)
dynamicPointerCast<CWindowTarget>(target)->window()->setHidden(true);
return;
}
case GEOMETRY_NO_DESIRED: {
// add a default geom
windowGeometry = CBox{WORK_AREA.middle() - DEFAULT_SIZE / 2.F, DEFAULT_SIZE};
break;
}
}
} else {
if (DESIRED_GEOM->pos)
windowGeometry = CBox{DESIRED_GEOM->pos.value(), DESIRED_GEOM->size};
else
windowGeometry = CBox{WORK_AREA.middle() - DESIRED_GEOM->size / 2.F, DESIRED_GEOM->size};
}
bool posOverridden = false;
if (target->window() && target->window()->m_firstMap) {
const auto WINDOW = target->window();
// set this here so that expressions can use it. This could be wrong of course.
WINDOW->m_realSize->setValueAndWarp(DESIRED_GEOM ? DESIRED_GEOM->size : DEFAULT_SIZE);
if (!WINDOW->m_ruleApplicator->static_.size.empty()) {
const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.size);
if (!COMPUTED)
Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.size);
else {
windowGeometry.w = COMPUTED->x;
windowGeometry.h = COMPUTED->y;
// update for pos to work with size.
WINDOW->m_realPosition->setValueAndWarp(*COMPUTED);
}
}
if (!WINDOW->m_ruleApplicator->static_.position.empty()) {
const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.position);
if (!COMPUTED)
Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.position);
else {
windowGeometry.x = COMPUTED->x + MONITOR_POS.x;
windowGeometry.y = COMPUTED->y + MONITOR_POS.y;
posOverridden = true;
}
}
if (WINDOW->m_ruleApplicator->static_.center.value_or(false)) {
const auto POS = WORK_AREA.middle() - windowGeometry.size() / 2.f;
windowGeometry.x = POS.x;
windowGeometry.y = POS.y;
posOverridden = true;
}
} else if (target->lastFloatingSize().x > 5 && target->lastFloatingSize().y > 5) {
windowGeometry.w = target->lastFloatingSize().x;
windowGeometry.h = target->lastFloatingSize().y;
}
if (!posOverridden && (!DESIRED_GEOM || !DESIRED_GEOM->pos))
windowGeometry = CBox{WORK_AREA.middle() - windowGeometry.size() / 2.F, windowGeometry.size()};
if (posOverridden || WORK_AREA.containsPoint(windowGeometry.middle()))
target->setPositionGlobal(windowGeometry);
else {
const auto POS = WORK_AREA.middle() - windowGeometry.size() / 2.f;
windowGeometry.x = POS.x;
windowGeometry.y = POS.y;
target->setPositionGlobal(windowGeometry);
}
// TODO: not very OOP, is it?
if (const auto WTARGET = dynamicPointerCast<CWindowTarget>(target); WTARGET) {
static auto PXWLFORCESCALEZERO = CConfigValue<Hyprlang::INT>("xwayland:force_zero_scaling");
const auto PWINDOW = WTARGET->window();
const auto PMONITOR = WTARGET->space()->workspace()->m_monitor.lock();
if (*PXWLFORCESCALEZERO && PWINDOW->m_isX11)
*PWINDOW->m_realSize = PWINDOW->m_realSize->goal() / PMONITOR->m_scale;
if (PWINDOW->m_X11DoesntWantBorders || (PWINDOW->m_isX11 && PWINDOW->isX11OverrideRedirect())) {
PWINDOW->m_realPosition->warp();
PWINDOW->m_realSize->warp();
}
if (!PWINDOW->isX11OverrideRedirect())
g_pCompositor->changeWindowZOrder(PWINDOW, true);
else {
PWINDOW->m_pendingReportedSize = PWINDOW->m_realSize->goal();
PWINDOW->m_reportedSize = PWINDOW->m_pendingReportedSize;
}
}
}
void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
auto LAST_SIZE = target->lastFloatingSize();
const auto CURRENT_SIZE = target->position().size();
// ignore positioning a dragged target
if (g_layoutManager->dragController()->target() == target)
return;
if (LAST_SIZE.x < 5 || LAST_SIZE.y < 5) {
const auto DESIRED = target->desiredGeometry();
LAST_SIZE = DESIRED ? DESIRED->size : DEFAULT_SIZE;
}
if (target->wasTiling()) {
// Avoid floating toggles that don't change size, they aren't easily visible to the user
if (std::abs(LAST_SIZE.x - CURRENT_SIZE.x) < 5 && std::abs(LAST_SIZE.y - CURRENT_SIZE.y) < 5)
LAST_SIZE += Vector2D{10, 10};
// calculate new position
const auto OLD_CENTER = target->position().middle();
// put around the current center, fit in workArea
target->setPositionGlobal(fitBoxInWorkArea(CBox{OLD_CENTER - LAST_SIZE / 2.F, LAST_SIZE}, target));
} else {
// calculate new position
const auto THIS_MON_POS = m_parent->space()->workspace()->m_monitor->m_position;
const auto OLD_POS = target->position().pos();
const auto MON_FROM_OLD = g_pCompositor->getMonitorFromVector(OLD_POS);
const auto NEW_POS = MON_FROM_OLD ? OLD_POS - MON_FROM_OLD->m_position + THIS_MON_POS : OLD_POS;
// put around the current center, fit in workArea
target->setPositionGlobal(fitBoxInWorkArea(CBox{NEW_POS, LAST_SIZE}, target));
}
}
CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t) {
const auto WORK_AREA = m_parent->space()->workArea(true);
const auto EXTENTS = t->window() ? t->window()->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS) : SBoxExtents{};
CBox targetBox = box.copy().addExtents(EXTENTS);
targetBox.x = std::max(targetBox.x, WORK_AREA.x);
targetBox.y = std::max(targetBox.y, WORK_AREA.y);
if (targetBox.x + targetBox.w > WORK_AREA.x + WORK_AREA.w)
targetBox.x = WORK_AREA.x + WORK_AREA.w - targetBox.w;
if (targetBox.y + targetBox.h > WORK_AREA.y + WORK_AREA.h)
targetBox.y = WORK_AREA.y + WORK_AREA.h - targetBox.h;
return targetBox.addExtents(SBoxExtents{.topLeft = -EXTENTS.topLeft, .bottomRight = -EXTENTS.bottomRight});
}
void CDefaultFloatingAlgorithm::removeTarget(SP<ITarget> target) {
target->rememberFloatingSize(target->position().size());
}
void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
auto pos = target->position();
pos.w += Δ.x;
pos.h += Δ.y;
pos.translate(-Δ / 2.F);
target->setPositionGlobal(pos);
if (g_layoutManager->dragController()->target() == target)
target->warpPositionSize();
}
void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
auto pos = target->position();
pos.translate(Δ);
target->setPositionGlobal(pos);
if (g_layoutManager->dragController()->target() == target)
target->warpPositionSize();
}
void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto posABackup = a->position();
a->setPositionGlobal(b->position());
b->setPositionGlobal(posABackup);
}
void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
auto pos = t->position();
auto work = m_parent->space()->workArea(true);
const auto EXTENTS = t->window() ? t->window()->getWindowExtentsUnified(Desktop::View::RESERVED_EXTENTS) : SBoxExtents{};
switch (dir) {
case Math::DIRECTION_LEFT: pos.x = work.x + EXTENTS.topLeft.x; break;
case Math::DIRECTION_RIGHT: pos.x = work.x + work.w - pos.w - EXTENTS.bottomRight.x; break;
case Math::DIRECTION_UP: pos.y = work.y + EXTENTS.topLeft.y; break;
case Math::DIRECTION_DOWN: pos.y = work.y + work.h - pos.h - EXTENTS.bottomRight.y; break;
default: Log::logger->log(Log::ERR, "Invalid direction in CDefaultFloatingAlgorithm::moveTargetInDirection"); break;
}
t->setPositionGlobal(pos);
}

View file

@ -0,0 +1,26 @@
#include "../../FloatingAlgorithm.hpp"
namespace Layout {
class CAlgorithm;
}
namespace Layout::Floating {
class CDefaultFloatingAlgorithm : public IFloatingAlgorithm {
public:
CDefaultFloatingAlgorithm() = default;
virtual ~CDefaultFloatingAlgorithm() = default;
virtual void newTarget(SP<ITarget> target);
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void moveTarget(const Vector2D& Δ, SP<ITarget> target);
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
private:
CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> t);
};
};

View file

@ -0,0 +1,772 @@
#include "DwindleAlgorithm.hpp"
#include "../../Algorithm.hpp"
#include "../../../space/Space.hpp"
#include "../../../target/WindowTarget.hpp"
#include "../../../LayoutManager.hpp"
#include "../../../../config/ConfigValue.hpp"
#include "../../../../desktop/state/FocusState.hpp"
#include "../../../../helpers/Monitor.hpp"
#include "../../../../Compositor.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Layout;
using namespace Layout::Tiled;
struct Layout::Tiled::SDwindleNodeData {
WP<SDwindleNodeData> pParent;
bool isNode = false;
WP<ITarget> pTarget;
std::array<WP<SDwindleNodeData>, 2> children = {};
WP<SDwindleNodeData> self;
bool splitTop = false; // for preserve_split
CBox box = {0};
float splitRatio = 1.f;
bool valid = true;
bool ignoreFullscreenChecks = false;
// For list lookup
bool operator==(const SDwindleNodeData& rhs) const {
return pTarget.lock() == rhs.pTarget.lock() && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] && children[1] == rhs.children[1];
}
void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false) {
if (children[0]) {
static auto PSMARTSPLIT = CConfigValue<Hyprlang::INT>("dwindle:smart_split");
static auto PPRESERVESPLIT = CConfigValue<Hyprlang::INT>("dwindle:preserve_split");
static auto PFLMULT = CConfigValue<Hyprlang::FLOAT>("dwindle:split_width_multiplier");
if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0)
splitTop = box.h * *PFLMULT > box.w;
if (verticalOverride)
splitTop = true;
else if (horizontalOverride)
splitTop = false;
const auto SPLITSIDE = !splitTop;
if (SPLITSIDE) {
// split left/right
const float FIRSTSIZE = box.w / 2.0 * splitRatio;
children[0]->box = CBox{box.x, box.y, FIRSTSIZE, box.h}.noNegativeSize();
children[1]->box = CBox{box.x + FIRSTSIZE, box.y, box.w - FIRSTSIZE, box.h}.noNegativeSize();
} else {
// split top/bottom
const float FIRSTSIZE = box.h / 2.0 * splitRatio;
children[0]->box = CBox{box.x, box.y, box.w, FIRSTSIZE}.noNegativeSize();
children[1]->box = CBox{box.x, box.y + FIRSTSIZE, box.w, box.h - FIRSTSIZE}.noNegativeSize();
}
children[0]->recalcSizePosRecursive(force);
children[1]->recalcSizePosRecursive(force);
} else
pTarget->setPositionGlobal(box);
}
};
void CDwindleAlgorithm::newTarget(SP<ITarget> target) {
addTarget(target);
}
void CDwindleAlgorithm::addTarget(SP<ITarget> target, bool newTarget) {
const auto WORK_AREA = m_parent->space()->workArea();
const auto PNODE = m_dwindleNodesData.emplace_back(makeShared<SDwindleNodeData>());
PNODE->self = PNODE;
const auto PMONITOR = m_parent->space()->workspace()->m_monitor;
const auto PWORKSPACE = m_parent->space()->workspace();
static auto PUSEACTIVE = CConfigValue<Hyprlang::INT>("dwindle:use_active_for_splits");
static auto PDEFAULTSPLIT = CConfigValue<Hyprlang::FLOAT>("dwindle:default_split_ratio");
// Populate the node with our window's data
PNODE->pTarget = target;
PNODE->isNode = false;
SP<SDwindleNodeData> OPENINGON;
const auto MOUSECOORDS = m_overrideFocalPoint.value_or(g_pInputManager->getMouseCoordsInternal());
const auto ACTIVE_MON = Desktop::focusState()->monitor();
if ((PWORKSPACE == ACTIVE_MON->m_activeWorkspace || (PWORKSPACE->m_isSpecialWorkspace && PMONITOR->m_activeSpecialWorkspace)) && !*PUSEACTIVE) {
OPENINGON = getNodeFromWindow(
g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::SKIP_FULLSCREEN_PRIORITY));
if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON))
OPENINGON = getClosestNode(MOUSECOORDS);
} else if (*PUSEACTIVE) {
if (Desktop::focusState()->window() && !Desktop::focusState()->window()->m_isFloating && Desktop::focusState()->window() != target->window() &&
Desktop::focusState()->window()->m_workspace == PWORKSPACE && Desktop::focusState()->window()->m_isMapped) {
OPENINGON = getNodeFromWindow(Desktop::focusState()->window());
} else {
OPENINGON = getNodeFromWindow(g_pCompositor->vectorToWindowUnified(MOUSECOORDS, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS));
}
if (!OPENINGON && g_pCompositor->isPointOnReservedArea(MOUSECOORDS, ACTIVE_MON))
OPENINGON = getClosestNode(MOUSECOORDS);
} else
OPENINGON = getFirstNode();
// first, check if OPENINGON isn't too big.
const auto PREDSIZEMAX = OPENINGON ? Vector2D(OPENINGON->box.w, OPENINGON->box.h) : PMONITOR->m_size;
if (const auto MAXSIZE = target->maxSize().value_or(Math::VECTOR2D_MAX); MAXSIZE.x < PREDSIZEMAX.x || MAXSIZE.y < PREDSIZEMAX.y) {
// we can't continue. make it floating.
std::erase(m_dwindleNodesData, PNODE);
m_parent->setFloating(target, true, true);
return;
}
// last fail-safe to avoid duplicate fullscreens
if ((!OPENINGON || OPENINGON->pTarget.lock() == target) && getNodes() > 1) {
for (auto& node : m_dwindleNodesData) {
if (node->pTarget.lock() && node->pTarget.lock() != target) {
OPENINGON = node;
break;
}
}
}
// if it's the first, it's easy. Make it fullscreen.
if (!OPENINGON || OPENINGON->pTarget.lock() == target) {
PNODE->box = WORK_AREA;
PNODE->pTarget->setPositionGlobal(PNODE->box);
return;
}
// get the node under our cursor
const auto NEWPARENT = m_dwindleNodesData.emplace_back(makeShared<SDwindleNodeData>());
// make the parent have the OPENINGON's stats
NEWPARENT->box = OPENINGON->box;
NEWPARENT->pParent = OPENINGON->pParent;
NEWPARENT->isNode = true; // it is a node
NEWPARENT->splitRatio = std::clamp(*PDEFAULTSPLIT, 0.1F, 1.9F);
static auto PWIDTHMULTIPLIER = CConfigValue<Hyprlang::FLOAT>("dwindle:split_width_multiplier");
// if cursor over first child, make it first, etc
const auto SIDEBYSIDE = NEWPARENT->box.w > NEWPARENT->box.h * *PWIDTHMULTIPLIER;
NEWPARENT->splitTop = !SIDEBYSIDE;
static auto PFORCESPLIT = CConfigValue<Hyprlang::INT>("dwindle:force_split");
static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue<Hyprlang::INT>("dwindle:permanent_direction_override");
static auto PSMARTSPLIT = CConfigValue<Hyprlang::INT>("dwindle:smart_split");
static auto PSPLITBIAS = CConfigValue<Hyprlang::INT>("dwindle:split_bias");
bool horizontalOverride = false;
bool verticalOverride = false;
// let user select position -> top, right, bottom, left
if (m_overrideDirection != Math::DIRECTION_DEFAULT) {
// this is horizontal
if (m_overrideDirection % 2 == 0)
verticalOverride = true;
else
horizontalOverride = true;
// 0 -> top and left | 1,2 -> right and bottom
if (m_overrideDirection % 3 == 0) {
NEWPARENT->children[1] = OPENINGON;
NEWPARENT->children[0] = PNODE;
} else {
NEWPARENT->children[0] = OPENINGON;
NEWPARENT->children[1] = PNODE;
}
// whether or not the override persists after opening one window
if (*PERMANENTDIRECTIONOVERRIDE == 0)
m_overrideDirection = Math::DIRECTION_DEFAULT;
} else if (*PSMARTSPLIT == 1) {
const auto PARENT_CENTER = NEWPARENT->box.pos() + NEWPARENT->box.size() / 2;
const auto PARENT_PROPORTIONS = NEWPARENT->box.h / NEWPARENT->box.w;
const auto DELTA = MOUSECOORDS - PARENT_CENTER;
const auto DELTA_SLOPE = DELTA.y / DELTA.x;
if (abs(DELTA_SLOPE) < PARENT_PROPORTIONS) {
if (DELTA.x > 0) {
// right
NEWPARENT->splitTop = false;
NEWPARENT->children[0] = OPENINGON;
NEWPARENT->children[1] = PNODE;
} else {
// left
NEWPARENT->splitTop = false;
NEWPARENT->children[0] = PNODE;
NEWPARENT->children[1] = OPENINGON;
}
} else {
if (DELTA.y > 0) {
// bottom
NEWPARENT->splitTop = true;
NEWPARENT->children[0] = OPENINGON;
NEWPARENT->children[1] = PNODE;
} else {
// top
NEWPARENT->splitTop = true;
NEWPARENT->children[0] = PNODE;
NEWPARENT->children[1] = OPENINGON;
}
}
} else if (*PFORCESPLIT == 0 || !newTarget) {
if ((SIDEBYSIDE &&
VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y + NEWPARENT->box.h)) ||
(!SIDEBYSIDE &&
VECINRECT(MOUSECOORDS, NEWPARENT->box.x, NEWPARENT->box.y / *PWIDTHMULTIPLIER, NEWPARENT->box.x + NEWPARENT->box.w, NEWPARENT->box.y + NEWPARENT->box.h / 2.f))) {
// we are hovering over the first node, make PNODE first.
NEWPARENT->children[1] = OPENINGON;
NEWPARENT->children[0] = PNODE;
} else {
// we are hovering over the second node, make PNODE second.
NEWPARENT->children[0] = OPENINGON;
NEWPARENT->children[1] = PNODE;
}
} else {
if (*PFORCESPLIT == 1) {
NEWPARENT->children[1] = OPENINGON;
NEWPARENT->children[0] = PNODE;
} else {
NEWPARENT->children[0] = OPENINGON;
NEWPARENT->children[1] = PNODE;
}
}
// split in favor of a specific window
if (*PSPLITBIAS && NEWPARENT->children[0] == PNODE)
NEWPARENT->splitRatio = 2.f - NEWPARENT->splitRatio;
// and update the previous parent if it exists
if (OPENINGON->pParent) {
if (OPENINGON->pParent->children[0] == OPENINGON) {
OPENINGON->pParent->children[0] = NEWPARENT;
} else {
OPENINGON->pParent->children[1] = NEWPARENT;
}
}
// Update the children
if (!verticalOverride && (NEWPARENT->box.w * *PWIDTHMULTIPLIER > NEWPARENT->box.h || horizontalOverride)) {
// split left/right -> forced
OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)};
PNODE->box = {Vector2D(NEWPARENT->box.x + NEWPARENT->box.w / 2.f, NEWPARENT->box.y), Vector2D(NEWPARENT->box.w / 2.f, NEWPARENT->box.h)};
} else {
// split top/bottom
OPENINGON->box = {NEWPARENT->box.pos(), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)};
PNODE->box = {Vector2D(NEWPARENT->box.x, NEWPARENT->box.y + NEWPARENT->box.h / 2.f), Vector2D(NEWPARENT->box.w, NEWPARENT->box.h / 2.f)};
}
OPENINGON->pParent = NEWPARENT;
PNODE->pParent = NEWPARENT;
NEWPARENT->recalcSizePosRecursive(false, horizontalOverride, verticalOverride);
calculateWorkspace();
}
void CDwindleAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
m_overrideFocalPoint = focalPoint;
addTarget(target, false);
m_overrideFocalPoint.reset();
}
void CDwindleAlgorithm::removeTarget(SP<ITarget> target) {
const auto PNODE = getNodeFromTarget(target);
if (!PNODE) {
Log::logger->log(Log::ERR, "onWindowRemovedTiling node null?");
return;
}
if (target->fullscreenMode() != FSMODE_NONE)
g_pCompositor->setWindowFullscreenInternal(target->window(), FSMODE_NONE);
const auto PPARENT = PNODE->pParent;
if (!PPARENT) {
Log::logger->log(Log::DEBUG, "Removing last node (dwindle)");
std::erase(m_dwindleNodesData, PNODE);
return;
}
const auto PSIBLING = PPARENT->children[0] == PNODE ? PPARENT->children[1] : PPARENT->children[0];
PSIBLING->pParent = PPARENT->pParent;
if (PPARENT->pParent != nullptr) {
if (PPARENT->pParent->children[0] == PPARENT)
PPARENT->pParent->children[0] = PSIBLING;
else
PPARENT->pParent->children[1] = PSIBLING;
}
PPARENT->valid = false;
PNODE->valid = false;
std::erase(m_dwindleNodesData, PPARENT);
std::erase(m_dwindleNodesData, PNODE);
recalculate();
}
void CDwindleAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
if (!validMapped(target->window()))
return;
const auto PNODE = getNodeFromTarget(target);
if (!PNODE)
return;
static auto PANIMATE = CConfigValue<Hyprlang::INT>("misc:animate_manual_resizes");
static auto PSMARTRESIZING = CConfigValue<Hyprlang::INT>("dwindle:smart_resizing");
// get some data about our window
const auto PMONITOR = m_parent->space()->workspace()->m_monitor;
const auto MONITOR_WORKAREA = PMONITOR->logicalBoxMinusReserved();
const auto BOX = target->position();
const bool DISPLAYLEFT = STICKS(BOX.x, MONITOR_WORKAREA.x);
const bool DISPLAYRIGHT = STICKS(BOX.x + BOX.w, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w);
const bool DISPLAYTOP = STICKS(BOX.y, MONITOR_WORKAREA.y);
const bool DISPLAYBOTTOM = STICKS(BOX.y + BOX.h, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h);
// construct allowed movement
Vector2D allowedMovement = Δ;
if (DISPLAYLEFT && DISPLAYRIGHT)
allowedMovement.x = 0;
if (DISPLAYBOTTOM && DISPLAYTOP)
allowedMovement.y = 0;
if (*PSMARTRESIZING == 1) {
// Identify inner and outer nodes for both directions
SP<SDwindleNodeData> PVOUTER = nullptr;
SP<SDwindleNodeData> PVINNER = nullptr;
SP<SDwindleNodeData> PHOUTER = nullptr;
SP<SDwindleNodeData> PHINNER = nullptr;
const auto LEFT = corner == CORNER_TOPLEFT || corner == CORNER_BOTTOMLEFT || DISPLAYRIGHT;
const auto TOP = corner == CORNER_TOPLEFT || corner == CORNER_TOPRIGHT || DISPLAYBOTTOM;
const auto RIGHT = corner == CORNER_TOPRIGHT || corner == CORNER_BOTTOMRIGHT || DISPLAYLEFT;
const auto BOTTOM = corner == CORNER_BOTTOMLEFT || corner == CORNER_BOTTOMRIGHT || DISPLAYTOP;
const auto NONE = corner == CORNER_NONE;
for (auto PCURRENT = PNODE; PCURRENT && PCURRENT->pParent; PCURRENT = PCURRENT->pParent.lock()) {
const auto PPARENT = PCURRENT->pParent;
if (!PVOUTER && PPARENT->splitTop && (NONE || (TOP && PPARENT->children[1] == PCURRENT) || (BOTTOM && PPARENT->children[0] == PCURRENT)))
PVOUTER = PCURRENT;
else if (!PVOUTER && !PVINNER && PPARENT->splitTop)
PVINNER = PCURRENT;
else if (!PHOUTER && !PPARENT->splitTop && (NONE || (LEFT && PPARENT->children[1] == PCURRENT) || (RIGHT && PPARENT->children[0] == PCURRENT)))
PHOUTER = PCURRENT;
else if (!PHOUTER && !PHINNER && !PPARENT->splitTop)
PHINNER = PCURRENT;
if (PVOUTER && PHOUTER)
break;
}
if (PHOUTER) {
PHOUTER->pParent->splitRatio = std::clamp(PHOUTER->pParent->splitRatio + allowedMovement.x * 2.f / PHOUTER->pParent->box.w, 0.1, 1.9);
if (PHINNER) {
const auto ORIGINAL = PHINNER->box.w;
PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
if (PHINNER->pParent->children[0] == PHINNER)
PHINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9);
else
PHINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.x) / PHINNER->pParent->box.w * 2.f, 0.1, 1.9);
PHINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
} else
PHOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
}
if (PVOUTER) {
PVOUTER->pParent->splitRatio = std::clamp(PVOUTER->pParent->splitRatio + allowedMovement.y * 2.f / PVOUTER->pParent->box.h, 0.1, 1.9);
if (PVINNER) {
const auto ORIGINAL = PVINNER->box.h;
PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
if (PVINNER->pParent->children[0] == PVINNER)
PVINNER->pParent->splitRatio = std::clamp((ORIGINAL - allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9);
else
PVINNER->pParent->splitRatio = std::clamp(2 - (ORIGINAL + allowedMovement.y) / PVINNER->pParent->box.h * 2.f, 0.1, 1.9);
PVINNER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
} else
PVOUTER->pParent->recalcSizePosRecursive(*PANIMATE == 0);
}
} else {
// get the correct containers to apply splitratio to
const auto PPARENT = PNODE->pParent;
if (!PPARENT)
return; // the only window on a workspace, ignore
const bool PARENTSIDEBYSIDE = !PPARENT->splitTop;
// Get the parent's parent
auto PPARENT2 = PPARENT->pParent;
Hyprutils::Utils::CScopeGuard x([target, this] {
// snap all windows, don't animate resizes if they are manual
if (target == g_layoutManager->dragController()->target()) {
for (const auto& w : m_dwindleNodesData) {
if (w->isNode)
continue;
w->pTarget->warpPositionSize();
}
}
});
// No parent means we have only 2 windows, and thus one axis of freedom
if (!PPARENT2) {
if (PARENTSIDEBYSIDE) {
allowedMovement.x *= 2.f / PPARENT->box.w;
PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9);
PPARENT->recalcSizePosRecursive(*PANIMATE == 0);
} else {
allowedMovement.y *= 2.f / PPARENT->box.h;
PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9);
PPARENT->recalcSizePosRecursive(*PANIMATE == 0);
}
return;
}
// Get first parent with other split
while (PPARENT2 && PPARENT2->splitTop == !PARENTSIDEBYSIDE)
PPARENT2 = PPARENT2->pParent;
// no parent, one axis of freedom
if (!PPARENT2) {
if (PARENTSIDEBYSIDE) {
allowedMovement.x *= 2.f / PPARENT->box.w;
PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.x, 0.1, 1.9);
PPARENT->recalcSizePosRecursive(*PANIMATE == 0);
} else {
allowedMovement.y *= 2.f / PPARENT->box.h;
PPARENT->splitRatio = std::clamp(PPARENT->splitRatio + allowedMovement.y, 0.1, 1.9);
PPARENT->recalcSizePosRecursive(*PANIMATE == 0);
}
return;
}
// 2 axes of freedom
const auto SIDECONTAINER = PARENTSIDEBYSIDE ? PPARENT : PPARENT2;
const auto TOPCONTAINER = PARENTSIDEBYSIDE ? PPARENT2 : PPARENT;
allowedMovement.x *= 2.f / SIDECONTAINER->box.w;
allowedMovement.y *= 2.f / TOPCONTAINER->box.h;
SIDECONTAINER->splitRatio = std::clamp(SIDECONTAINER->splitRatio + allowedMovement.x, 0.1, 1.9);
TOPCONTAINER->splitRatio = std::clamp(TOPCONTAINER->splitRatio + allowedMovement.y, 0.1, 1.9);
SIDECONTAINER->recalcSizePosRecursive(*PANIMATE == 0);
TOPCONTAINER->recalcSizePosRecursive(*PANIMATE == 0);
}
// snap all windows, don't animate resizes if they are manual
if (target == g_layoutManager->dragController()->target()) {
for (const auto& w : m_dwindleNodesData) {
if (w->isNode)
continue;
w->pTarget->warpPositionSize();
}
}
}
SP<ITarget> CDwindleAlgorithm::getNextCandidate(SP<ITarget> old) {
const auto MIDDLE = old->position().middle();
if (const auto NODE = getClosestNode(MIDDLE); NODE)
return NODE->pTarget.lock();
if (const auto NODE = getFirstNode(); NODE)
return NODE->pTarget.lock();
return nullptr;
}
void CDwindleAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto nodeA = getNodeFromTarget(a);
auto nodeB = getNodeFromTarget(b);
if (nodeA)
nodeA->pTarget = b;
if (nodeB)
nodeB->pTarget = a;
}
void CDwindleAlgorithm::recalculate() {
calculateWorkspace();
}
std::optional<Vector2D> CDwindleAlgorithm::predictSizeForNewTarget() {
// get window candidate
PHLWINDOW candidate = Desktop::focusState()->window();
if (!candidate || candidate->m_workspace != m_parent->space()->workspace())
candidate = m_parent->space()->workspace()->getFirstWindow();
// create a fake node
SDwindleNodeData node;
if (!candidate)
return Desktop::focusState()->monitor()->m_size;
else {
const auto PNODE = getNodeFromWindow(candidate);
if (!PNODE)
return {};
node = *PNODE;
node.pTarget.reset();
CBox box = PNODE->box;
static auto PFLMULT = CConfigValue<Hyprlang::FLOAT>("dwindle:split_width_multiplier");
bool splitTop = box.h * *PFLMULT > box.w;
const auto SPLITSIDE = !splitTop;
if (SPLITSIDE)
node.box = {{}, {box.w / 2.0, box.h}};
else
node.box = {{}, {box.w, box.h / 2.0}};
// TODO: make this better and more accurate
return node.box.size();
}
return {};
}
void CDwindleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
const auto PNODE = getNodeFromTarget(t);
const Vector2D originalPos = t->position().middle();
if (!PNODE || !t->window())
return;
Vector2D focalPoint;
const auto WINDOWIDEALBB =
t->fullscreenMode() != FSMODE_NONE ? m_parent->space()->workspace()->m_monitor->logicalBox() : t->window()->getWindowIdealBoundingBoxIgnoreReserved();
switch (dir) {
case Math::DIRECTION_UP: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, -1.0}; break;
case Math::DIRECTION_DOWN: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x / 2.0, WINDOWIDEALBB.size().y + 1.0}; break;
case Math::DIRECTION_LEFT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{-1.0, WINDOWIDEALBB.size().y / 2.0}; break;
case Math::DIRECTION_RIGHT: focalPoint = WINDOWIDEALBB.pos() + Vector2D{WINDOWIDEALBB.size().x + 1.0, WINDOWIDEALBB.size().y / 2.0}; break;
default: return;
}
t->window()->setAnimationsToMove();
removeTarget(t);
const auto PMONITORFOCAL = g_pCompositor->getMonitorFromVector(focalPoint);
if (PMONITORFOCAL != m_parent->space()->workspace()->m_monitor) {
// move with a focal point
if (PMONITORFOCAL->m_activeWorkspace)
t->assignToSpace(PMONITORFOCAL->m_activeWorkspace->m_space);
return;
}
movedTarget(t, focalPoint);
// restore focus to the previous position
if (silent) {
const auto PNODETOFOCUS = getClosestNode(originalPos);
if (PNODETOFOCUS && PNODETOFOCUS->pTarget)
Desktop::focusState()->fullWindowFocus(PNODETOFOCUS->pTarget->window(), Desktop::FOCUS_REASON_KEYBIND);
}
}
// --------- internal --------- //
void CDwindleAlgorithm::calculateWorkspace() {
const auto PWORKSPACE = m_parent->space()->workspace();
const auto PMONITOR = PWORKSPACE->m_monitor;
if (!PMONITOR || PWORKSPACE->m_hasFullscreenWindow)
return;
const auto TOPNODE = getMasterNode();
if (TOPNODE) {
TOPNODE->box = m_parent->space()->workArea();
TOPNODE->recalcSizePosRecursive();
}
}
SP<SDwindleNodeData> CDwindleAlgorithm::getNodeFromTarget(SP<ITarget> t) {
for (const auto& n : m_dwindleNodesData) {
if (n->pTarget == t)
return n;
}
return nullptr;
}
SP<SDwindleNodeData> CDwindleAlgorithm::getNodeFromWindow(PHLWINDOW w) {
return w ? getNodeFromTarget(w->layoutTarget()) : nullptr;
}
int CDwindleAlgorithm::getNodes() {
return m_dwindleNodesData.size();
}
SP<SDwindleNodeData> CDwindleAlgorithm::getFirstNode() {
return m_dwindleNodesData.empty() ? nullptr : m_dwindleNodesData.at(0);
}
SP<SDwindleNodeData> CDwindleAlgorithm::getClosestNode(const Vector2D& point) {
SP<SDwindleNodeData> res = nullptr;
double distClosest = -1;
for (auto& n : m_dwindleNodesData) {
if (n->pTarget && Desktop::View::validMapped(n->pTarget->window())) {
auto distAnother = vecToRectDistanceSquared(point, n->box.pos(), n->box.pos() + n->box.size());
if (!res || distAnother < distClosest) {
res = n;
distClosest = distAnother;
}
}
}
return res;
}
SP<SDwindleNodeData> CDwindleAlgorithm::getMasterNode() {
for (auto& n : m_dwindleNodesData) {
if (!n->pParent)
return n;
}
return nullptr;
}
std::expected<void, std::string> CDwindleAlgorithm::layoutMsg(const std::string_view& sv) {
const auto ARGS = CVarList2(std::string{sv}, 0, ' ');
const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window());
if (ARGS[0] == "togglesplit") {
if (CURRENT_NODE)
toggleSplit(CURRENT_NODE);
} else if (ARGS[0] == "swapsplit") {
if (CURRENT_NODE)
swapSplit(CURRENT_NODE);
} else if (ARGS[0] == "movetoroot") {
auto node = CURRENT_NODE;
if (!ARGS[1].empty()) {
auto w = g_pCompositor->getWindowByRegex(std::string{ARGS[1]});
if (w)
node = getNodeFromWindow(w);
}
const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable";
moveToRoot(node, STABLE);
} else if (ARGS[0] == "preselect") {
auto direction = ARGS[1];
if (direction.empty()) {
Log::logger->log(Log::ERR, "Expected direction for preselect");
return std::unexpected("No direction for preselect");
}
switch (direction.front()) {
case 'u':
case 't': {
m_overrideDirection = Math::DIRECTION_UP;
break;
}
case 'd':
case 'b': {
m_overrideDirection = Math::DIRECTION_DOWN;
break;
}
case 'r': {
m_overrideDirection = Math::DIRECTION_RIGHT;
break;
}
case 'l': {
m_overrideDirection = Math::DIRECTION_LEFT;
break;
}
default: {
// any other character resets the focus direction
// needed for the persistent mode
m_overrideDirection = Math::DIRECTION_DEFAULT;
break;
}
}
}
return {};
}
void CDwindleAlgorithm::toggleSplit(SP<SDwindleNodeData> x) {
if (!x || !x->pParent)
return;
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return;
x->pParent->splitTop = !x->pParent->splitTop;
x->pParent->recalcSizePosRecursive();
}
void CDwindleAlgorithm::swapSplit(SP<SDwindleNodeData> x) {
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return;
std::swap(x->pParent->children[0], x->pParent->children[1]);
x->pParent->recalcSizePosRecursive();
}
void CDwindleAlgorithm::moveToRoot(SP<SDwindleNodeData> x, bool stable) {
if (!x || !x->pParent)
return;
if (x->pTarget->fullscreenMode() != FSMODE_NONE)
return;
// already at root
if (!x->pParent->pParent)
return;
auto& pNode = x->pParent->children[0] == x ? x->pParent->children[0] : x->pParent->children[1];
// instead of [getMasterNodeOnWorkspace], we walk back to root since we need
// to know which children of root is our ancestor
auto pAncestor = x, pRoot = x->pParent.lock();
while (pRoot->pParent) {
pAncestor = pRoot;
pRoot = pRoot->pParent.lock();
}
auto& pSwap = pRoot->children[0] == pAncestor ? pRoot->children[1] : pRoot->children[0];
std::swap(pNode, pSwap);
std::swap(pNode->pParent, pSwap->pParent);
// [stable] in that the focused window occupies same side of screen
if (stable)
std::swap(pRoot->children[0], pRoot->children[1]);
pRoot->recalcSizePosRecursive();
}

View file

@ -0,0 +1,57 @@
#include "../../TiledAlgorithm.hpp"
namespace Layout {
class CAlgorithm;
}
namespace Layout::Tiled {
struct SDwindleNodeData;
class CDwindleAlgorithm : public ITiledAlgorithm {
public:
CDwindleAlgorithm() = default;
virtual ~CDwindleAlgorithm() = default;
virtual void newTarget(SP<ITarget> target);
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
virtual std::optional<Vector2D> predictSizeForNewTarget();
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
private:
std::vector<SP<SDwindleNodeData>> m_dwindleNodesData;
struct {
bool started = false;
bool pseudo = false;
bool xExtent = false;
bool yExtent = false;
} m_pseudoDragFlags;
std::optional<Vector2D> m_overrideFocalPoint; // for onWindowCreatedTiling.
void addTarget(SP<ITarget> target, bool newTarget = true);
void calculateWorkspace();
SP<SDwindleNodeData> getNodeFromTarget(SP<ITarget>);
SP<SDwindleNodeData> getNodeFromWindow(PHLWINDOW w);
int getNodes();
SP<SDwindleNodeData> getFirstNode();
SP<SDwindleNodeData> getClosestNode(const Vector2D&);
SP<SDwindleNodeData> getMasterNode();
void toggleSplit(SP<SDwindleNodeData>);
void swapSplit(SP<SDwindleNodeData>);
void moveToRoot(SP<SDwindleNodeData>, bool stable = true);
Math::eDirection m_overrideDirection = Math::DIRECTION_DEFAULT;
};
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
#include "../../TiledAlgorithm.hpp"
#include <hyprutils/string/VarList2.hpp>
namespace Layout {
class CAlgorithm;
}
namespace Layout::Tiled {
struct SMasterNodeData;
//orientation determines which side of the screen the master area resides
enum eOrientation : uint8_t {
ORIENTATION_LEFT = 0,
ORIENTATION_TOP,
ORIENTATION_RIGHT,
ORIENTATION_BOTTOM,
ORIENTATION_CENTER
};
struct SMasterWorkspaceData {
WORKSPACEID workspaceID = WORKSPACE_INVALID;
eOrientation orientation = ORIENTATION_LEFT;
// Previously focused non-master window when `focusmaster previous` command was issued
WP<ITarget> focusMasterPrev;
//
bool operator==(const SMasterWorkspaceData& rhs) const {
return workspaceID == rhs.workspaceID;
}
};
class CMasterAlgorithm : public ITiledAlgorithm {
public:
CMasterAlgorithm() = default;
virtual ~CMasterAlgorithm() = default;
virtual void newTarget(SP<ITarget> target);
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
virtual std::optional<Vector2D> predictSizeForNewTarget();
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
private:
std::vector<SP<SMasterNodeData>> m_masterNodesData;
SMasterWorkspaceData m_workspaceData;
void addTarget(SP<ITarget> target, bool firstMap);
bool m_forceWarps = false;
void buildOrientationCycleVectorFromVars(std::vector<eOrientation>& cycle, Hyprutils::String::CVarList2* vars);
void buildOrientationCycleVectorFromEOperation(std::vector<eOrientation>& cycle);
void runOrientationCycle(Hyprutils::String::CVarList2* vars, int next);
eOrientation getDynamicOrientation();
int getNodesNo();
SP<SMasterNodeData> getNodeFromWindow(PHLWINDOW);
SP<SMasterNodeData> getNodeFromTarget(SP<ITarget>);
SP<SMasterNodeData> getMasterNode();
SP<SMasterNodeData> getClosestNode(const Vector2D&);
void calculateWorkspace();
SP<ITarget> getNextTarget(SP<ITarget>, bool, bool);
int getMastersNo();
bool isWindowTiled(PHLWINDOW);
};
};

View file

@ -0,0 +1,274 @@
#include "MonocleAlgorithm.hpp"
#include "../../Algorithm.hpp"
#include "../../../space/Space.hpp"
#include "../../../target/WindowTarget.hpp"
#include "../../../LayoutManager.hpp"
#include "../../../../config/ConfigValue.hpp"
#include "../../../../desktop/state/FocusState.hpp"
#include "../../../../desktop/history/WindowHistoryTracker.hpp"
#include "../../../../helpers/Monitor.hpp"
#include "../../../../Compositor.hpp"
#include <hyprutils/string/VarList2.hpp>
#include <hyprutils/string/ConstVarList.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::Utils;
using namespace Layout;
using namespace Layout::Tiled;
CMonocleAlgorithm::CMonocleAlgorithm() {
// hook into focus changes to bring focused window to front
m_focusCallback = g_pHookSystem->hookDynamic("activeWindow", [this](void* hk, SCallbackInfo& info, std::any param) {
const auto PWINDOW = std::any_cast<Desktop::View::SWindowActiveEvent>(param).window;
if (!PWINDOW)
return;
if (!PWINDOW->m_workspace->isVisible())
return;
const auto TARGET = PWINDOW->layoutTarget();
if (!TARGET)
return;
focusTargetUpdate(TARGET);
});
}
CMonocleAlgorithm::~CMonocleAlgorithm() {
// unhide all windows before destruction
for (const auto& data : m_targetDatas) {
const auto TARGET = data->target.lock();
if (!TARGET)
continue;
const auto WINDOW = TARGET->window();
if (WINDOW)
WINDOW->setHidden(false);
}
m_focusCallback.reset();
}
SP<SMonocleTargetData> CMonocleAlgorithm::dataFor(SP<ITarget> t) {
for (auto& data : m_targetDatas) {
if (data->target.lock() == t)
return data;
}
return nullptr;
}
void CMonocleAlgorithm::newTarget(SP<ITarget> target) {
const auto DATA = m_targetDatas.emplace_back(makeShared<SMonocleTargetData>(target));
m_currentVisibleIndex = m_targetDatas.size() - 1;
recalculate();
}
void CMonocleAlgorithm::movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint) {
newTarget(target);
}
void CMonocleAlgorithm::removeTarget(SP<ITarget> target) {
auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; });
if (it == m_targetDatas.end())
return;
// unhide window when removing from monocle layout
const auto WINDOW = target->window();
if (WINDOW)
WINDOW->setHidden(false);
const auto INDEX = std::distance(m_targetDatas.begin(), it);
m_targetDatas.erase(it);
if (m_targetDatas.empty()) {
m_currentVisibleIndex = 0;
return;
}
// try to use the last window in history if we can
for (const auto& historyWindow : Desktop::History::windowTracker()->historyForWorkspace(m_parent->space()->workspace()) | std::views::reverse) {
auto it = std::ranges::find_if(m_targetDatas, [&historyWindow](const auto& d) { return d->target == historyWindow->layoutTarget(); });
if (it == m_targetDatas.end())
continue;
// we found a historical target, use that first
m_currentVisibleIndex = std::distance(m_targetDatas.begin(), it);
recalculate();
return;
}
// if we didn't find history, fall back to last
if (m_currentVisibleIndex >= (int)m_targetDatas.size())
m_currentVisibleIndex = m_targetDatas.size() - 1;
else if (INDEX <= m_currentVisibleIndex && m_currentVisibleIndex > 0)
m_currentVisibleIndex--;
recalculate();
}
void CMonocleAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
// monocle layout doesn't support manual resizing, all windows are fullscreen
}
void CMonocleAlgorithm::recalculate() {
if (m_targetDatas.empty())
return;
const auto WORK_AREA = m_parent->space()->workArea();
for (size_t i = 0; i < m_targetDatas.size(); ++i) {
const auto& DATA = m_targetDatas[i];
const auto TARGET = DATA->target.lock();
if (!TARGET)
continue;
const auto WINDOW = TARGET->window();
if (!WINDOW)
continue;
DATA->layoutBox = WORK_AREA;
TARGET->setPositionGlobal(WORK_AREA);
const bool SHOULD_BE_VISIBLE = ((int)i == m_currentVisibleIndex);
WINDOW->setHidden(!SHOULD_BE_VISIBLE);
}
}
SP<ITarget> CMonocleAlgorithm::getNextCandidate(SP<ITarget> old) {
if (m_targetDatas.empty())
return nullptr;
auto it = std::ranges::find_if(m_targetDatas, [old](const auto& data) { return data->target.lock() == old; });
if (it == m_targetDatas.end()) {
if (m_currentVisibleIndex >= 0 && m_currentVisibleIndex < (int)m_targetDatas.size())
return m_targetDatas[m_currentVisibleIndex]->target.lock();
return nullptr;
}
auto next = std::next(it);
if (next == m_targetDatas.end())
next = m_targetDatas.begin();
return next->get()->target.lock();
}
std::expected<void, std::string> CMonocleAlgorithm::layoutMsg(const std::string_view& sv) {
CVarList2 vars(std::string{sv}, 0, 's');
if (vars.size() < 1)
return std::unexpected("layoutmsg requires at least 1 argument");
const auto COMMAND = vars[0];
if (COMMAND == "cyclenext") {
cycleNext();
return {};
} else if (COMMAND == "cycleprev") {
cyclePrev();
return {};
}
return std::unexpected(std::format("Unknown monocle layoutmsg: {}", COMMAND));
}
std::optional<Vector2D> CMonocleAlgorithm::predictSizeForNewTarget() {
const auto WORK_AREA = m_parent->space()->workArea();
return WORK_AREA.size();
}
void CMonocleAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
auto nodeA = dataFor(a);
auto nodeB = dataFor(b);
if (nodeA)
nodeA->target = b;
if (nodeB)
nodeB->target = a;
recalculate();
}
void CMonocleAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
// try to find a monitor in the specified direction, thats the logical thing
if (!t || !t->space() || !t->space()->workspace())
return;
const auto PMONINDIR = g_pCompositor->getMonitorInDirection(t->space()->workspace()->m_monitor.lock(), dir);
// if we found a monitor, move the window there
if (PMONINDIR && PMONINDIR != t->space()->workspace()->m_monitor.lock()) {
const auto TARGETWS = PMONINDIR->m_activeWorkspace;
if (t->window())
t->window()->setAnimationsToMove();
t->assignToSpace(TARGETWS->m_space);
}
}
void CMonocleAlgorithm::cycleNext() {
if (m_targetDatas.empty())
return;
m_currentVisibleIndex = (m_currentVisibleIndex + 1) % m_targetDatas.size();
updateVisible();
}
void CMonocleAlgorithm::cyclePrev() {
if (m_targetDatas.empty())
return;
m_currentVisibleIndex--;
if (m_currentVisibleIndex < 0)
m_currentVisibleIndex = m_targetDatas.size() - 1;
updateVisible();
}
void CMonocleAlgorithm::focusTargetUpdate(SP<ITarget> target) {
auto it = std::ranges::find_if(m_targetDatas, [target](const auto& data) { return data->target.lock() == target; });
if (it == m_targetDatas.end())
return;
const auto NEW_INDEX = std::distance(m_targetDatas.begin(), it);
if (m_currentVisibleIndex != NEW_INDEX) {
m_currentVisibleIndex = NEW_INDEX;
updateVisible();
}
}
void CMonocleAlgorithm::updateVisible() {
recalculate();
const auto VISIBLE_TARGET = getVisibleTarget();
if (!VISIBLE_TARGET)
return;
const auto WINDOW = VISIBLE_TARGET->window();
if (!WINDOW)
return;
Desktop::focusState()->fullWindowFocus(WINDOW, Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
}
SP<ITarget> CMonocleAlgorithm::getVisibleTarget() {
if (m_currentVisibleIndex < 0 || m_currentVisibleIndex >= (int)m_targetDatas.size())
return nullptr;
return m_targetDatas[m_currentVisibleIndex]->target.lock();
}

View file

@ -0,0 +1,52 @@
#pragma once
#include "../../TiledAlgorithm.hpp"
#include "../../../../managers/HookSystemManager.hpp"
#include <vector>
namespace Layout::Tiled {
struct SMonocleTargetData {
SMonocleTargetData(SP<ITarget> t) : target(t) {
;
}
WP<ITarget> target;
CBox layoutBox;
};
class CMonocleAlgorithm : public ITiledAlgorithm {
public:
CMonocleAlgorithm();
virtual ~CMonocleAlgorithm();
virtual void newTarget(SP<ITarget> target);
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
virtual std::optional<Vector2D> predictSizeForNewTarget();
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
private:
std::vector<SP<SMonocleTargetData>> m_targetDatas;
SP<HOOK_CALLBACK_FN> m_focusCallback;
int m_currentVisibleIndex = 0;
SP<SMonocleTargetData> dataFor(SP<ITarget> t);
void cycleNext();
void cyclePrev();
void focusTargetUpdate(SP<ITarget> target);
void updateVisible();
SP<ITarget> getVisibleTarget();
};
};

View file

@ -0,0 +1,293 @@
#include "ScrollTapeController.hpp"
#include "ScrollingAlgorithm.hpp"
#include <algorithm>
#include <cmath>
using namespace Layout::Tiled;
CScrollTapeController::CScrollTapeController(eScrollDirection direction) : m_direction(direction) {
;
}
void CScrollTapeController::setDirection(eScrollDirection dir) {
m_direction = dir;
}
eScrollDirection CScrollTapeController::getDirection() const {
return m_direction;
}
bool CScrollTapeController::isPrimaryHorizontal() const {
return m_direction == SCROLL_DIR_RIGHT || m_direction == SCROLL_DIR_LEFT;
}
bool CScrollTapeController::isReversed() const {
return m_direction == SCROLL_DIR_LEFT || m_direction == SCROLL_DIR_UP;
}
size_t CScrollTapeController::stripCount() const {
return m_strips.size();
}
SStripData& CScrollTapeController::getStrip(size_t index) {
return m_strips[index];
}
const SStripData& CScrollTapeController::getStrip(size_t index) const {
return m_strips[index];
}
void CScrollTapeController::setOffset(double offset) {
m_offset = offset;
}
double CScrollTapeController::getOffset() const {
return m_offset;
}
void CScrollTapeController::adjustOffset(double delta) {
m_offset += delta;
}
size_t CScrollTapeController::addStrip(float size) {
m_strips.emplace_back();
m_strips.back().size = size;
return m_strips.size() - 1;
}
void CScrollTapeController::insertStrip(size_t afterIndex, float size) {
if (afterIndex >= m_strips.size()) {
addStrip(size);
return;
}
SStripData newStrip;
newStrip.size = size;
m_strips.insert(m_strips.begin() + afterIndex + 1, newStrip);
}
void CScrollTapeController::removeStrip(size_t index) {
if (index < m_strips.size())
m_strips.erase(m_strips.begin() + index);
}
double CScrollTapeController::getPrimary(const Vector2D& v) const {
return isPrimaryHorizontal() ? v.x : v.y;
}
double CScrollTapeController::getSecondary(const Vector2D& v) const {
return isPrimaryHorizontal() ? v.y : v.x;
}
void CScrollTapeController::setPrimary(Vector2D& v, double val) const {
if (isPrimaryHorizontal())
v.x = val;
else
v.y = val;
}
void CScrollTapeController::setSecondary(Vector2D& v, double val) const {
if (isPrimaryHorizontal())
v.y = val;
else
v.x = val;
}
Vector2D CScrollTapeController::makeVector(double primary, double secondary) const {
if (isPrimaryHorizontal())
return {primary, secondary};
else
return {secondary, primary};
}
double CScrollTapeController::calculateMaxExtent(const CBox& usableArea, bool fullscreenOnOne) const {
if (m_strips.empty())
return 0.0;
if (fullscreenOnOne && m_strips.size() == 1)
return getPrimary(usableArea.size());
double total = 0.0;
const double usablePrimary = getPrimary(usableArea.size());
for (const auto& strip : m_strips) {
total += usablePrimary * strip.size;
}
return total;
}
double CScrollTapeController::calculateStripStart(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const {
if (stripIndex >= m_strips.size())
return 0.0;
const double usablePrimary = getPrimary(usableArea.size());
double current = 0.0;
for (size_t i = 0; i < stripIndex; ++i) {
const double stripSize = (fullscreenOnOne && m_strips.size() == 1) ? usablePrimary : usablePrimary * m_strips[i].size;
current += stripSize;
}
return current;
}
double CScrollTapeController::calculateStripSize(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const {
if (stripIndex >= m_strips.size())
return 0.0;
const double usablePrimary = getPrimary(usableArea.size());
if (fullscreenOnOne && m_strips.size() == 1)
return usablePrimary;
return usablePrimary * m_strips[stripIndex].size;
}
CBox CScrollTapeController::calculateTargetBox(size_t stripIndex, size_t targetIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne) {
if (stripIndex >= m_strips.size())
return {};
const auto& strip = m_strips[stripIndex];
if (targetIndex >= strip.targetSizes.size())
return {};
const double usableSecondary = getSecondary(usableArea.size());
const double usablePrimary = getPrimary(usableArea.size());
const double cameraOffset = calculateCameraOffset(usableArea, fullscreenOnOne);
// calculate position along primary axis (strip position)
double primaryPos = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
double primarySize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);
// calculate position along secondary axis (within strip)
double secondaryPos = 0.0;
for (size_t i = 0; i < targetIndex; ++i) {
secondaryPos += strip.targetSizes[i] * usableSecondary;
}
double secondarySize = strip.targetSizes[targetIndex] * usableSecondary;
// apply camera offset based on direction
// for RIGHT/DOWN: scroll offset moves content left/up (subtract)
// for LEFT/UP: scroll offset moves content right/down (different coordinate system)
if (m_direction == SCROLL_DIR_LEFT) {
// LEFT: flip the entire primary axis, then apply offset
primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset;
} else if (m_direction == SCROLL_DIR_UP) {
// UP: flip the entire primary axis, then apply offset
primaryPos = usablePrimary - primaryPos - primarySize + cameraOffset;
} else {
// RIGHT/DOWN: normal offset
primaryPos -= cameraOffset;
}
// create the box in primary/secondary coordinates
Vector2D pos = makeVector(primaryPos, secondaryPos);
Vector2D size = makeVector(primarySize, secondarySize);
// translate to workspace position
pos = pos + workspaceOffset;
return CBox{pos, size};
}
double CScrollTapeController::calculateCameraOffset(const CBox& usableArea, bool fullscreenOnOne) {
const double maxExtent = calculateMaxExtent(usableArea, fullscreenOnOne);
const double usablePrimary = getPrimary(usableArea.size());
// don't adjust the offset if we are dragging
if (isBeingDragged())
return m_offset;
// if the content fits in viewport, center it
if (maxExtent < usablePrimary)
m_offset = std::round((maxExtent - usablePrimary) / 2.0);
// if the offset is negative but we already extended, reset offset to 0
if (maxExtent > usablePrimary && m_offset < 0.0)
m_offset = 0.0;
return m_offset;
}
Vector2D CScrollTapeController::getCameraTranslation(const CBox& usableArea, bool fullscreenOnOne) {
const double offset = calculateCameraOffset(usableArea, fullscreenOnOne);
if (isReversed())
return makeVector(offset, 0.0);
else
return makeVector(-offset, 0.0);
}
void CScrollTapeController::centerStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) {
if (stripIndex >= m_strips.size())
return;
const double usablePrimary = getPrimary(usableArea.size());
const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
const double stripSize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);
m_offset = stripStart - (usablePrimary - stripSize) / 2.0;
}
void CScrollTapeController::fitStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) {
if (stripIndex >= m_strips.size())
return;
const double usablePrimary = getPrimary(usableArea.size());
const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
const double stripSize = calculateStripSize(stripIndex, usableArea, fullscreenOnOne);
m_offset = std::clamp(m_offset, stripStart - usablePrimary + stripSize, stripStart);
}
bool CScrollTapeController::isStripVisible(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne) const {
if (stripIndex >= m_strips.size())
return false;
const double stripStart = calculateStripStart(stripIndex, usableArea, fullscreenOnOne);
const double stripEnd = stripStart + calculateStripSize(stripIndex, usableArea, fullscreenOnOne);
const double viewStart = m_offset;
const double viewEnd = m_offset + getPrimary(usableArea.size());
return stripStart < viewEnd && viewStart < stripEnd;
}
size_t CScrollTapeController::getStripAtCenter(const CBox& usableArea, bool fullscreenOnOne) const {
if (m_strips.empty())
return 0;
const double usablePrimary = getPrimary(usableArea.size());
double currentPos = m_offset;
for (size_t i = 0; i < m_strips.size(); ++i) {
const double stripSize = calculateStripSize(i, usableArea, fullscreenOnOne);
currentPos += stripSize;
if (currentPos >= usablePrimary / 2.0 - 2.0)
return i;
}
return m_strips.empty() ? 0 : m_strips.size() - 1;
}
void CScrollTapeController::swapStrips(size_t a, size_t b) {
if (a >= m_strips.size() || b >= m_strips.size())
return;
std::swap(m_strips.at(a), m_strips.at(b));
}
bool CScrollTapeController::isBeingDragged() const {
for (const auto& s : m_strips) {
if (!s.userData)
continue;
for (const auto& d : s.userData->targetDatas) {
if (d->target == g_layoutManager->dragController()->target())
return true;
}
}
return false;
}

View file

@ -0,0 +1,83 @@
#pragma once
#include "../../../../helpers/math/Math.hpp"
#include "../../../../helpers/memory/Memory.hpp"
#include <vector>
namespace Layout::Tiled {
struct SColumnData;
enum eScrollDirection : uint8_t {
SCROLL_DIR_RIGHT = 0,
SCROLL_DIR_LEFT,
SCROLL_DIR_DOWN,
SCROLL_DIR_UP,
};
struct SStripData {
float size = 1.F; // size along primary axis
std::vector<float> targetSizes; // sizes along secondary axis for each target in this strip
WP<SColumnData> userData;
SStripData() = default;
};
struct STapeLayoutResult {
CBox box;
size_t stripIndex = 0;
size_t targetIndex = 0;
};
class CScrollTapeController {
public:
CScrollTapeController(eScrollDirection direction = SCROLL_DIR_RIGHT);
~CScrollTapeController() = default;
void setDirection(eScrollDirection dir);
eScrollDirection getDirection() const;
bool isPrimaryHorizontal() const;
bool isReversed() const;
size_t addStrip(float size = 1.0F);
void insertStrip(size_t afterIndex, float size = 1.0F);
void removeStrip(size_t index);
size_t stripCount() const;
SStripData& getStrip(size_t index);
const SStripData& getStrip(size_t index) const;
void swapStrips(size_t a, size_t b);
void setOffset(double offset);
double getOffset() const;
void adjustOffset(double delta);
double calculateMaxExtent(const CBox& usableArea, bool fullscreenOnOne = false) const;
double calculateStripStart(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const;
double calculateStripSize(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const;
CBox calculateTargetBox(size_t stripIndex, size_t targetIndex, const CBox& usableArea, const Vector2D& workspaceOffset, bool fullscreenOnOne = false);
double calculateCameraOffset(const CBox& usableArea, bool fullscreenOnOne = false);
Vector2D getCameraTranslation(const CBox& usableArea, bool fullscreenOnOne = false);
void centerStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false);
void fitStrip(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false);
bool isStripVisible(size_t stripIndex, const CBox& usableArea, bool fullscreenOnOne = false) const;
size_t getStripAtCenter(const CBox& usableArea, bool fullscreenOnOne = false) const;
private:
eScrollDirection m_direction = SCROLL_DIR_RIGHT;
std::vector<SStripData> m_strips;
double m_offset = 0.0;
double getPrimary(const Vector2D& v) const;
double getSecondary(const Vector2D& v) const;
void setPrimary(Vector2D& v, double val) const;
void setSecondary(Vector2D& v, double val) const;
bool isBeingDragged() const;
Vector2D makeVector(double primary, double secondary) const;
};
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,137 @@
#pragma once
#include "../../TiledAlgorithm.hpp"
#include "../../../../managers/HookSystemManager.hpp"
#include "../../../../helpers/math/Direction.hpp"
#include "ScrollTapeController.hpp"
#include <vector>
namespace Layout::Tiled {
class CScrollingAlgorithm;
struct SColumnData;
struct SScrollingData;
struct SScrollingTargetData {
SScrollingTargetData(SP<ITarget> t, SP<SColumnData> col) : target(t), column(col) {
;
}
WP<ITarget> target;
WP<SColumnData> column;
bool ignoreFullscreenChecks = false;
CBox layoutBox;
};
struct SColumnData {
SColumnData(SP<SScrollingData> data) : scrollingData(data) {
;
}
void add(SP<ITarget> t);
void add(SP<ITarget> t, int after);
void add(SP<SScrollingTargetData> w);
void add(SP<SScrollingTargetData> w, int after);
void remove(SP<ITarget> t);
bool has(SP<ITarget> t);
size_t idx(SP<ITarget> t);
// index of lowest target that is above y.
size_t idxForHeight(float y);
void up(SP<SScrollingTargetData> w);
void down(SP<SScrollingTargetData> w);
SP<SScrollingTargetData> next(SP<SScrollingTargetData> w);
SP<SScrollingTargetData> prev(SP<SScrollingTargetData> w);
std::vector<SP<SScrollingTargetData>> targetDatas;
WP<SScrollingData> scrollingData;
WP<SScrollingTargetData> lastFocusedTarget;
WP<SColumnData> self;
// Helper methods to access controller-managed data
float getColumnWidth() const;
void setColumnWidth(float width);
float getTargetSize(size_t idx) const;
void setTargetSize(size_t idx, float size);
float getTargetSize(SP<SScrollingTargetData> target) const;
void setTargetSize(SP<SScrollingTargetData> target, float size);
};
struct SScrollingData {
SScrollingData(CScrollingAlgorithm* algo);
std::vector<SP<SColumnData>> columns;
UP<CScrollTapeController> controller;
SP<SColumnData> add();
SP<SColumnData> add(int after);
int64_t idx(SP<SColumnData> c);
void remove(SP<SColumnData> c);
double maxWidth();
SP<SColumnData> next(SP<SColumnData> c);
SP<SColumnData> prev(SP<SColumnData> c);
SP<SColumnData> atCenter();
bool visible(SP<SColumnData> c);
void centerCol(SP<SColumnData> c);
void fitCol(SP<SColumnData> c);
void centerOrFitCol(SP<SColumnData> c);
void recalculate(bool forceInstant = false);
CScrollingAlgorithm* algorithm = nullptr;
WP<SScrollingData> self;
std::optional<double> lockedCameraOffset;
};
class CScrollingAlgorithm : public ITiledAlgorithm {
public:
CScrollingAlgorithm();
virtual ~CScrollingAlgorithm();
virtual void newTarget(SP<ITarget> target);
virtual void movedTarget(SP<ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
virtual std::expected<void, std::string> layoutMsg(const std::string_view& sv);
virtual std::optional<Vector2D> predictSizeForNewTarget();
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b);
virtual void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
CBox usableArea();
private:
SP<SScrollingData> m_scrollingData;
SP<HOOK_CALLBACK_FN> m_configCallback;
SP<HOOK_CALLBACK_FN> m_focusCallback;
SP<HOOK_CALLBACK_FN> m_mouseButtonCallback;
struct {
std::vector<float> configuredWidths;
} m_config;
eScrollDirection getDynamicDirection();
SP<SScrollingTargetData> findBestNeighbor(SP<SScrollingTargetData> pCurrent, SP<SColumnData> pTargetCol);
SP<SScrollingTargetData> dataFor(SP<ITarget> t);
SP<SScrollingTargetData> closestNode(const Vector2D& posGlobglobgabgalab);
void focusTargetUpdate(SP<ITarget> target);
void moveTargetTo(SP<ITarget> t, Math::eDirection dir, bool silent);
void focusOnInput(SP<ITarget> target, bool hardInput);
friend struct SScrollingData;
};
};

185
src/layout/space/Space.cpp Normal file
View file

@ -0,0 +1,185 @@
#include "Space.hpp"
#include "../target/Target.hpp"
#include "../algorithm/Algorithm.hpp"
#include "../../debug/log/Logger.hpp"
#include "../../desktop/Workspace.hpp"
#include "../../config/ConfigManager.hpp"
using namespace Layout;
SP<CSpace> CSpace::create(PHLWORKSPACE w) {
auto space = SP<CSpace>(new CSpace(w));
space->m_self = space;
return space;
}
CSpace::CSpace(PHLWORKSPACE parent) : m_parent(parent) {
recheckWorkArea();
}
void CSpace::add(SP<ITarget> t) {
m_targets.emplace_back(t);
recheckWorkArea();
if (m_algorithm)
m_algorithm->addTarget(t);
m_parent->updateWindows();
}
void CSpace::move(SP<ITarget> t, std::optional<Vector2D> focalPoint) {
m_targets.emplace_back(t);
recheckWorkArea();
if (m_algorithm)
m_algorithm->moveTarget(t, focalPoint);
m_parent->updateWindows();
}
void CSpace::remove(SP<ITarget> t) {
std::erase_if(m_targets, [&t](const auto& e) { return !e || e == t; });
recheckWorkArea();
if (m_algorithm)
m_algorithm->removeTarget(t);
if (m_parent) // can be null if the workspace is gone
m_parent->updateWindows();
}
void CSpace::setAlgorithmProvider(SP<CAlgorithm> algo) {
m_algorithm = algo;
}
void CSpace::recheckWorkArea() {
if (!m_parent || !m_parent->m_monitor) {
Log::logger->log(Log::ERR, "CSpace: recheckWorkArea on no parent / mon?!");
return;
}
const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(m_parent.lock());
auto workArea = m_parent->m_monitor->logicalBoxMinusReserved();
static auto PGAPSOUTDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_out");
static auto PFLOATGAPSDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:float_gaps");
auto* const PGAPSOUT = sc<CCssGapData*>((PGAPSOUTDATA.ptr())->getData());
auto* PFLOATGAPS = sc<CCssGapData*>(PFLOATGAPSDATA.ptr()->getData());
if (PFLOATGAPS->m_bottom < 0 || PFLOATGAPS->m_left < 0 || PFLOATGAPS->m_right < 0 || PFLOATGAPS->m_top < 0)
PFLOATGAPS = PGAPSOUT;
auto gapsOut = WORKSPACERULE.gapsOut.value_or(*PGAPSOUT);
auto gapsFloat = WORKSPACERULE.gapsOut.value_or(*PFLOATGAPS);
Desktop::CReservedArea reservedGaps{gapsOut.m_top, gapsOut.m_right, gapsOut.m_bottom, gapsOut.m_left};
Desktop::CReservedArea reservedFloatGaps{gapsFloat.m_top, gapsFloat.m_right, gapsFloat.m_bottom, gapsFloat.m_left};
auto floatWorkArea = workArea;
reservedFloatGaps.applyip(floatWorkArea);
reservedGaps.applyip(workArea);
m_workArea = workArea;
m_floatingWorkArea = floatWorkArea;
}
const CBox& CSpace::workArea(bool floating) const {
return floating ? m_floatingWorkArea : m_workArea;
}
PHLWORKSPACE CSpace::workspace() const {
return m_parent.lock();
}
void CSpace::toggleTargetFloating(SP<ITarget> t) {
t->setWasTiling(true);
m_algorithm->setFloating(t, !t->floating());
t->setWasTiling(false);
m_parent->updateWindows();
recalculate();
}
CBox CSpace::targetPositionLocal(SP<ITarget> t) const {
return t->position().translate(-m_workArea.pos());
}
void CSpace::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner) {
if (!m_algorithm)
return;
m_algorithm->resizeTarget(Δ, target, corner);
}
void CSpace::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
if (!m_algorithm)
return;
m_algorithm->moveTarget(Δ, target);
}
SP<CAlgorithm> CSpace::algorithm() const {
return m_algorithm;
}
void CSpace::recalculate() {
recheckWorkArea();
if (m_algorithm)
m_algorithm->recalculate();
}
void CSpace::setFullscreen(SP<ITarget> t, eFullscreenMode mode) {
t->setFullscreenMode(mode);
if (mode == FSMODE_NONE && m_algorithm && t->floating())
m_algorithm->recenter(t);
recalculate();
}
std::expected<void, std::string> CSpace::layoutMsg(const std::string_view& sv) {
if (m_algorithm)
return m_algorithm->layoutMsg(sv);
return {};
}
std::optional<Vector2D> CSpace::predictSizeForNewTiledTarget() {
if (m_algorithm)
return m_algorithm->predictSizeForNewTiledTarget();
return std::nullopt;
}
void CSpace::swap(SP<ITarget> a, SP<ITarget> b) {
for (auto& t : m_targets) {
if (t == a)
t = b;
else if (t == b)
t = a;
}
if (m_algorithm)
m_algorithm->swapTargets(a, b);
}
void CSpace::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent) {
if (m_algorithm)
m_algorithm->moveTargetInDirection(t, dir, silent);
}
SP<ITarget> CSpace::getNextCandidate(SP<ITarget> old) {
return !m_algorithm ? nullptr : m_algorithm->getNextCandidate(old);
}
const std::vector<WP<ITarget>>& CSpace::targets() const {
return m_targets;
}

View file

@ -0,0 +1,67 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/math/Direction.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "../../desktop/DesktopTypes.hpp"
#include "../LayoutManager.hpp"
#include <optional>
#include <expected>
namespace Layout {
class ITarget;
class CAlgorithm;
class CSpace {
public:
static SP<CSpace> create(PHLWORKSPACE w);
~CSpace() = default;
void add(SP<ITarget> t);
void remove(SP<ITarget> t);
void move(SP<ITarget> t, std::optional<Vector2D> focalPoint = std::nullopt);
void swap(SP<ITarget> a, SP<ITarget> b);
SP<ITarget> getNextCandidate(SP<ITarget> old);
void setAlgorithmProvider(SP<CAlgorithm> algo);
void recheckWorkArea();
void setFullscreen(SP<ITarget> t, eFullscreenMode mode);
void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
void recalculate();
void toggleTargetFloating(SP<ITarget> t);
std::expected<void, std::string> layoutMsg(const std::string_view& sv);
std::optional<Vector2D> predictSizeForNewTiledTarget();
const CBox& workArea(bool floating = false) const;
PHLWORKSPACE workspace() const;
CBox targetPositionLocal(SP<ITarget> t) const;
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
void moveTarget(const Vector2D& Δ, SP<ITarget> target);
SP<CAlgorithm> algorithm() const;
const std::vector<WP<ITarget>>& targets() const;
private:
CSpace(PHLWORKSPACE parent);
WP<CSpace> m_self;
std::vector<WP<ITarget>> m_targets;
SP<CAlgorithm> m_algorithm;
PHLWORKSPACEREF m_parent;
// work area is in global coords
CBox m_workArea, m_floatingWorkArea;
};
};

View file

@ -0,0 +1,396 @@
#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();
}

View file

@ -0,0 +1,54 @@
#pragma once
#include "../target/Target.hpp"
#include "../../managers/input/InputManager.hpp"
namespace Layout {
enum eRectCorner : uint8_t;
}
namespace Layout::Supplementary {
// DragStateController contains logic to begin and end a drag, which shouldn't be part of the layout's job. It's stuff like
// toggling float when dragging tiled, remembering sizes, checking deltas, etc.
class CDragStateController {
public:
CDragStateController() = default;
~CDragStateController() = default;
void dragBegin(SP<ITarget> target, eMouseBindMode mode);
void dragEnd();
void mouseMove(const Vector2D& mousePos);
eMouseBindMode mode() const;
bool wasDraggingWindow() const;
bool dragThresholdReached() const;
void resetDragThresholdReached();
bool draggingTiled() const;
/*
Called to try to pick up window for dragging.
Updates drag related variables and floats window if threshold reached.
Return true to reject
*/
bool updateDragWindow();
SP<ITarget> target() const;
private:
WP<ITarget> m_target;
eMouseBindMode m_dragMode = MBIND_INVALID;
bool m_wasDraggingWindow = false;
bool m_dragThresholdReached = false;
bool m_draggingTiled = false;
int m_mouseMoveEventCount = 0;
Vector2D m_beginDragXY;
Vector2D m_lastDragXY;
Vector2D m_beginDragPositionXY;
Vector2D m_beginDragSizeXY;
Vector2D m_draggingWindowOriginalFloatSize;
Layout::eRectCorner m_grabbedCorner = sc<Layout::eRectCorner>(0) /* CORNER_NONE */;
};
};

View file

@ -0,0 +1,139 @@
#include "WorkspaceAlgoMatcher.hpp"
#include "../../config/ConfigValue.hpp"
#include "../../config/ConfigManager.hpp"
#include "../algorithm/Algorithm.hpp"
#include "../space/Space.hpp"
#include "../algorithm/floating/default/DefaultFloatingAlgorithm.hpp"
#include "../algorithm/tiled/dwindle/DwindleAlgorithm.hpp"
#include "../algorithm/tiled/master/MasterAlgorithm.hpp"
#include "../algorithm/tiled/scrolling/ScrollingAlgorithm.hpp"
#include "../algorithm/tiled/monocle/MonocleAlgorithm.hpp"
#include "../../Compositor.hpp"
using namespace Layout;
using namespace Layout::Supplementary;
constexpr const char* DEFAULT_FLOATING_ALGO = "default";
constexpr const char* DEFAULT_TILED_ALGO = "dwindle";
const UP<CWorkspaceAlgoMatcher>& Supplementary::algoMatcher() {
static UP<CWorkspaceAlgoMatcher> m = makeUnique<CWorkspaceAlgoMatcher>();
return m;
}
CWorkspaceAlgoMatcher::CWorkspaceAlgoMatcher() {
m_tiledAlgos = {
{"dwindle", [] { return makeUnique<Tiled::CDwindleAlgorithm>(); }},
{"master", [] { return makeUnique<Tiled::CMasterAlgorithm>(); }},
{"scrolling", [] { return makeUnique<Tiled::CScrollingAlgorithm>(); }},
{"monocle", [] { return makeUnique<Tiled::CMonocleAlgorithm>(); }},
};
m_floatingAlgos = {
{"default", [] { return makeUnique<Floating::CDefaultFloatingAlgorithm>(); }},
};
m_algoNames = {
{&typeid(Tiled::CDwindleAlgorithm), "dwindle"},
{&typeid(Tiled::CMasterAlgorithm), "master"},
{&typeid(Tiled::CScrollingAlgorithm), "scrolling"},
{&typeid(Tiled::CMonocleAlgorithm), "monocle"},
{&typeid(Floating::CDefaultFloatingAlgorithm), "default"},
};
}
bool CWorkspaceAlgoMatcher::registerTiledAlgo(const std::string& name, const std::type_info* typeInfo, std::function<UP<ITiledAlgorithm>()>&& factory) {
if (m_tiledAlgos.contains(name) || m_floatingAlgos.contains(name))
return false;
m_tiledAlgos.emplace(name, std::move(factory));
m_algoNames.emplace(typeInfo, name);
updateWorkspaceLayouts();
return true;
}
bool CWorkspaceAlgoMatcher::registerFloatingAlgo(const std::string& name, const std::type_info* typeInfo, std::function<UP<IFloatingAlgorithm>()>&& factory) {
if (m_tiledAlgos.contains(name) || m_floatingAlgos.contains(name))
return false;
m_floatingAlgos.emplace(name, std::move(factory));
m_algoNames.emplace(typeInfo, name);
updateWorkspaceLayouts();
return true;
}
bool CWorkspaceAlgoMatcher::unregisterAlgo(const std::string& name) {
if (!m_tiledAlgos.contains(name) && !m_floatingAlgos.contains(name))
return false;
std::erase_if(m_algoNames, [&name](const auto& e) { return e.second == name; });
if (m_floatingAlgos.contains(name))
m_floatingAlgos.erase(name);
else
m_tiledAlgos.erase(name);
// this is needed here to avoid situations where a plugin unloads and we still have a UP
// to a plugin layout
updateWorkspaceLayouts();
return true;
}
UP<ITiledAlgorithm> CWorkspaceAlgoMatcher::algoForNameTiled(const std::string& s) {
if (m_tiledAlgos.contains(s))
return m_tiledAlgos.at(s)();
return m_tiledAlgos.at(DEFAULT_TILED_ALGO)();
}
UP<IFloatingAlgorithm> CWorkspaceAlgoMatcher::algoForNameFloat(const std::string& s) {
if (m_floatingAlgos.contains(s))
return m_floatingAlgos.at(s)();
return m_floatingAlgos.at(DEFAULT_FLOATING_ALGO)();
}
std::string CWorkspaceAlgoMatcher::tiledAlgoForWorkspace(const PHLWORKSPACE& w) {
static auto PLAYOUT = CConfigValue<Hyprlang::STRING>("general:layout");
auto rule = g_pConfigManager->getWorkspaceRuleFor(w);
return rule.layout.value_or(*PLAYOUT);
}
SP<CAlgorithm> CWorkspaceAlgoMatcher::createAlgorithmForWorkspace(PHLWORKSPACE w) {
return CAlgorithm::create(algoForNameTiled(tiledAlgoForWorkspace(w)), makeUnique<Floating::CDefaultFloatingAlgorithm>(), w->m_space);
}
void CWorkspaceAlgoMatcher::updateWorkspaceLayouts() {
// TODO: make this ID-based, string comparison is slow
for (const auto& ws : g_pCompositor->getWorkspaces()) {
if (!ws)
continue;
const auto& TILED_ALGO = ws->m_space->algorithm()->tiledAlgo();
if (!TILED_ALGO)
continue;
const auto LAYOUT_TO_USE = tiledAlgoForWorkspace(ws.lock());
if (m_algoNames.contains(&typeid(*TILED_ALGO.get())) && m_algoNames.at(&typeid(*TILED_ALGO.get())) == LAYOUT_TO_USE)
continue;
// needs a switchup
ws->m_space->algorithm()->updateTiledAlgo(algoForNameTiled(LAYOUT_TO_USE));
}
}
std::string CWorkspaceAlgoMatcher::getNameForTiledAlgo(const std::type_info* type) {
if (m_algoNames.contains(type))
return m_algoNames.at(type);
return "unknown";
}

View file

@ -0,0 +1,46 @@
#pragma once
#include "../../desktop/DesktopTypes.hpp"
#include <map>
#include <type_traits>
#include <functional>
#include <string>
namespace Layout {
class CAlgorithm;
class ITiledAlgorithm;
class IFloatingAlgorithm;
}
namespace Layout::Supplementary {
class CWorkspaceAlgoMatcher {
public:
CWorkspaceAlgoMatcher();
~CWorkspaceAlgoMatcher() = default;
SP<CAlgorithm> createAlgorithmForWorkspace(PHLWORKSPACE w);
void updateWorkspaceLayouts();
std::string getNameForTiledAlgo(const std::type_info* type);
// these fns can fail due to name collisions
bool registerTiledAlgo(const std::string& name, const std::type_info* typeInfo, std::function<UP<ITiledAlgorithm>()>&& factory);
bool registerFloatingAlgo(const std::string& name, const std::type_info* typeInfo, std::function<UP<IFloatingAlgorithm>()>&& factory);
// this fn fails if the algo isn't registered
bool unregisterAlgo(const std::string& name);
private:
UP<ITiledAlgorithm> algoForNameTiled(const std::string& s);
UP<IFloatingAlgorithm> algoForNameFloat(const std::string& s);
std::string tiledAlgoForWorkspace(const PHLWORKSPACE&);
std::map<std::string, std::function<UP<ITiledAlgorithm>()>> m_tiledAlgos;
std::map<std::string, std::function<UP<IFloatingAlgorithm>()>> m_floatingAlgos;
std::map<const std::type_info*, std::string> m_algoNames;
};
const UP<CWorkspaceAlgoMatcher>& algoMatcher();
}

View file

@ -0,0 +1,146 @@
#include "Target.hpp"
#include "../space/Space.hpp"
#include "../../debug/log/Logger.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Layout;
using namespace Hyprutils::Utils;
void ITarget::setPositionGlobal(const CBox& box) {
m_box = box;
m_box.round();
}
void ITarget::assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint) {
if (m_space == space && !m_ghostSpace)
return;
const bool HAD_SPACE = !!m_space;
if (m_space && !m_ghostSpace)
m_space->remove(m_self.lock());
m_space = space;
if (space && HAD_SPACE)
space->move(m_self.lock(), focalPoint);
else if (space)
space->add(m_self.lock());
if (!space)
Log::logger->log(Log::WARN, "ITarget: assignToSpace with a null space?");
m_ghostSpace = false;
onUpdateSpace();
}
void ITarget::setSpaceGhost(const SP<CSpace>& space) {
if (m_space)
assignToSpace(nullptr);
m_space = space;
m_ghostSpace = true;
}
SP<CSpace> ITarget::space() const {
return m_space;
}
PHLWORKSPACE ITarget::workspace() const {
if (!m_space)
return nullptr;
return m_space->workspace();
}
CBox ITarget::position() const {
return m_box;
}
void ITarget::rememberFloatingSize(const Vector2D& size) {
m_floatingSize = size;
}
Vector2D ITarget::lastFloatingSize() const {
return m_floatingSize;
}
void ITarget::recalc() {
setPositionGlobal(m_box);
}
void ITarget::setPseudo(bool x) {
if (m_pseudo == x)
return;
m_pseudo = x;
recalc();
}
bool ITarget::isPseudo() const {
return m_pseudo;
}
void ITarget::setPseudoSize(const Vector2D& size) {
m_pseudoSize = size;
recalc();
}
Vector2D ITarget::pseudoSize() {
return m_pseudoSize;
}
void ITarget::swap(SP<ITarget> b) {
const auto IS_FLOATING = floating();
const auto IS_FLOATING_B = b->floating();
// Keep workspaces alive during a swap: moving one window will unref the ws
// NOLINTNEXTLINE
const auto PWS1 = workspace();
// NOLINTNEXTLINE
const auto PWS2 = b->workspace();
CScopeGuard x([&] {
b->setFloating(IS_FLOATING);
setFloating(IS_FLOATING_B);
// update the spaces
b->onUpdateSpace();
onUpdateSpace();
});
if (b->space() == m_space) {
// simplest
m_space->swap(m_self.lock(), b);
m_space->recalculate();
return;
}
// spaces differ
if (m_space)
m_space->swap(m_self.lock(), b);
if (b->space())
b->space()->swap(b, m_self.lock());
std::swap(m_space, b->m_space);
// recalc both
if (m_space)
m_space->recalculate();
if (b->space())
b->space()->recalculate();
}
bool ITarget::wasTiling() const {
return m_wasTiling;
}
void ITarget::setWasTiling(bool x) {
m_wasTiling = x;
}

View file

@ -0,0 +1,79 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "../../desktop/Workspace.hpp"
#include <expected>
#include <cstdint>
namespace Layout {
enum eTargetType : uint8_t {
TARGET_TYPE_WINDOW = 0,
TARGET_TYPE_GROUP,
};
enum eGeometryFailure : uint8_t {
GEOMETRY_NO_DESIRED = 0,
GEOMETRY_INVALID_DESIRED = 1,
};
class CSpace;
struct SGeometryRequested {
Vector2D size;
std::optional<Vector2D> pos;
};
class ITarget {
public:
virtual ~ITarget() = default;
virtual eTargetType type() = 0;
// position is within its space
virtual void setPositionGlobal(const CBox& box);
virtual CBox position() const;
virtual void assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint = std::nullopt);
virtual void setSpaceGhost(const SP<CSpace>& space);
virtual SP<CSpace> space() const;
virtual PHLWORKSPACE workspace() const;
virtual PHLWINDOW window() const = 0;
virtual void recalc();
virtual bool wasTiling() const;
virtual void setWasTiling(bool x);
virtual void rememberFloatingSize(const Vector2D& size);
virtual Vector2D lastFloatingSize() const;
virtual void setPseudo(bool x);
virtual bool isPseudo() const;
virtual void setPseudoSize(const Vector2D& size);
virtual Vector2D pseudoSize();
virtual void swap(SP<ITarget> b);
//
virtual bool floating() = 0;
virtual void setFloating(bool x) = 0;
virtual std::expected<SGeometryRequested, eGeometryFailure> desiredGeometry() = 0;
virtual eFullscreenMode fullscreenMode() = 0;
virtual void setFullscreenMode(eFullscreenMode mode) = 0;
virtual std::optional<Vector2D> minSize() = 0;
virtual std::optional<Vector2D> maxSize() = 0;
virtual void damageEntire() = 0;
virtual void warpPositionSize() = 0;
virtual void onUpdateSpace() = 0;
protected:
ITarget() = default;
CBox m_box;
SP<CSpace> m_space;
WP<ITarget> m_self;
Vector2D m_floatingSize;
bool m_pseudo = false;
bool m_ghostSpace = false; // ghost space means a target belongs to a space, but isn't sent to the layout
Vector2D m_pseudoSize = {1280, 720};
bool m_wasTiling = false;
};
};

View file

@ -0,0 +1,92 @@
#include "WindowGroupTarget.hpp"
#include "../space/Space.hpp"
#include "../algorithm/Algorithm.hpp"
#include "WindowTarget.hpp"
#include "Target.hpp"
#include "../../render/Renderer.hpp"
using namespace Layout;
SP<CWindowGroupTarget> CWindowGroupTarget::create(SP<Desktop::View::CGroup> g) {
auto target = SP<CWindowGroupTarget>(new CWindowGroupTarget(g));
target->m_self = target;
return target;
}
CWindowGroupTarget::CWindowGroupTarget(SP<Desktop::View::CGroup> g) : m_group(g) {
;
}
eTargetType CWindowGroupTarget::type() {
return TARGET_TYPE_GROUP;
}
void CWindowGroupTarget::setPositionGlobal(const CBox& box) {
ITarget::setPositionGlobal(box);
updatePos();
}
void CWindowGroupTarget::updatePos() {
for (const auto& w : m_group->windows()) {
w->m_target->setPositionGlobal(m_box);
}
}
void CWindowGroupTarget::assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint) {
ITarget::assignToSpace(space, focalPoint);
m_group->updateWorkspace(space->workspace());
}
bool CWindowGroupTarget::floating() {
return m_group->current()->m_target->floating();
}
void CWindowGroupTarget::setFloating(bool x) {
for (const auto& w : m_group->windows()) {
w->m_target->setFloating(x);
}
}
std::expected<SGeometryRequested, eGeometryFailure> CWindowGroupTarget::desiredGeometry() {
return m_group->current()->m_target->desiredGeometry();
}
PHLWINDOW CWindowGroupTarget::window() const {
return m_group->current();
}
eFullscreenMode CWindowGroupTarget::fullscreenMode() {
return m_group->current()->m_fullscreenState.internal;
}
void CWindowGroupTarget::setFullscreenMode(eFullscreenMode mode) {
m_group->current()->m_fullscreenState.internal = mode;
}
std::optional<Vector2D> CWindowGroupTarget::minSize() {
return m_group->current()->minSize();
}
std::optional<Vector2D> CWindowGroupTarget::maxSize() {
return m_group->current()->maxSize();
}
void CWindowGroupTarget::damageEntire() {
g_pHyprRenderer->damageWindow(m_group->current());
}
void CWindowGroupTarget::warpPositionSize() {
for (const auto& w : m_group->windows()) {
w->m_target->warpPositionSize();
}
}
void CWindowGroupTarget::onUpdateSpace() {
for (const auto& w : m_group->windows()) {
w->m_target->onUpdateSpace();
}
}

View file

@ -0,0 +1,39 @@
#pragma once
#include "Target.hpp"
#include "../../desktop/view/Window.hpp"
#include "../../desktop/view/Group.hpp"
namespace Layout {
class CWindowGroupTarget : public ITarget {
public:
static SP<CWindowGroupTarget> create(SP<Desktop::View::CGroup> g);
virtual ~CWindowGroupTarget() = default;
virtual eTargetType type();
virtual void setPositionGlobal(const CBox& box);
virtual void assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint = std::nullopt);
virtual PHLWINDOW window() const;
virtual bool floating();
virtual void setFloating(bool x);
virtual std::expected<SGeometryRequested, eGeometryFailure> desiredGeometry();
virtual eFullscreenMode fullscreenMode();
virtual void setFullscreenMode(eFullscreenMode mode);
virtual std::optional<Vector2D> minSize();
virtual std::optional<Vector2D> maxSize();
virtual void damageEntire();
virtual void warpPositionSize();
virtual void onUpdateSpace();
private:
CWindowGroupTarget(SP<Desktop::View::CGroup> g);
void updatePos();
WP<Desktop::View::CGroup> m_group;
};
};

View file

@ -0,0 +1,363 @@
#include "WindowTarget.hpp"
#include "../space/Space.hpp"
#include "../algorithm/Algorithm.hpp"
#include "../../protocols/core/Compositor.hpp"
#include "../../config/ConfigManager.hpp"
#include "../../helpers/Monitor.hpp"
#include "../../xwayland/XSurface.hpp"
#include "../../Compositor.hpp"
#include "../../render/Renderer.hpp"
using namespace Layout;
SP<ITarget> CWindowTarget::create(PHLWINDOW w) {
auto target = SP<CWindowTarget>(new CWindowTarget(w));
target->m_self = target;
return target;
}
CWindowTarget::CWindowTarget(PHLWINDOW w) : m_window(w) {
;
}
eTargetType CWindowTarget::type() {
return TARGET_TYPE_WINDOW;
}
void CWindowTarget::setPositionGlobal(const CBox& box) {
ITarget::setPositionGlobal(box);
updatePos();
}
void CWindowTarget::updatePos() {
if (!m_space)
return;
if (fullscreenMode() == FSMODE_FULLSCREEN)
return;
if (floating() && fullscreenMode() != FSMODE_MAXIMIZED) {
m_window->m_position = m_box.pos();
m_window->m_size = m_box.size();
*m_window->m_realPosition = m_box.pos();
*m_window->m_realSize = m_box.size();
m_window->sendWindowSize();
m_window->updateWindowDecos();
return;
}
// Tiled is more complicated.
const auto PMONITOR = m_space->workspace()->m_monitor;
const auto PWORKSPACE = m_space->workspace();
// for gaps outer
const auto MONITOR_WORKAREA = m_space->workArea();
const bool DISPLAYLEFT = STICKS(m_box.x, MONITOR_WORKAREA.x);
const bool DISPLAYRIGHT = STICKS(m_box.x + m_box.w, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w);
const bool DISPLAYTOP = STICKS(m_box.y, MONITOR_WORKAREA.y);
const bool DISPLAYBOTTOM = STICKS(m_box.y + m_box.h, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h);
// this is used for scrolling, so that the gaps are correct when a window is the full width and has neighbors
const bool DISPLAYINVERSELEFT = STICKS(m_box.x, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w);
const bool DISPLAYINVERSERIGHT = STICKS(m_box.x + m_box.w, MONITOR_WORKAREA.x);
// get specific gaps and rules for this workspace,
// if user specified them in config
const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(PWORKSPACE);
if (!validMapped(m_window)) {
if (m_window)
g_layoutManager->removeTarget(m_window->layoutTarget());
return;
}
if (fullscreenMode() == FSMODE_FULLSCREEN)
return;
g_pHyprRenderer->damageWindow(window());
static auto PGAPSINDATA = CConfigValue<Hyprlang::CUSTOMTYPE>("general:gaps_in");
auto* const PGAPSIN = sc<CCssGapData*>((PGAPSINDATA.ptr())->getData());
auto gapsIn = WORKSPACERULE.gapsIn.value_or(*PGAPSIN);
CBox nodeBox = m_box;
nodeBox.round();
m_window->m_size = nodeBox.size();
m_window->m_position = nodeBox.pos();
m_window->updateWindowDecos();
auto calcPos = m_window->m_position;
auto calcSize = m_window->m_size;
const static auto REQUESTEDRATIO = CConfigValue<Hyprlang::VEC2>("layout:single_window_aspect_ratio");
const static auto REQUESTEDRATIOTOLERANCE = CConfigValue<Hyprlang::FLOAT>("layout:single_window_aspect_ratio_tolerance");
Vector2D ratioPadding;
if ((*REQUESTEDRATIO).y != 0 && m_space->algorithm()->tiledTargets() <= 1) {
const Vector2D originalSize = MONITOR_WORKAREA.size();
const double requestedRatio = (*REQUESTEDRATIO).x / (*REQUESTEDRATIO).y;
const double originalRatio = originalSize.x / originalSize.y;
if (requestedRatio > originalRatio) {
double padding = originalSize.y - (originalSize.x / requestedRatio);
if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.y)
ratioPadding = Vector2D{0., padding};
} else if (requestedRatio < originalRatio) {
double padding = originalSize.x - (originalSize.y * requestedRatio);
if (padding / 2 > (*REQUESTEDRATIOTOLERANCE) * originalSize.x)
ratioPadding = Vector2D{padding, 0.};
}
}
const auto GAPOFFSETTOPLEFT = Vector2D(sc<double>(DISPLAYLEFT ? 0 : (DISPLAYINVERSELEFT ? 2 * gapsIn.m_left : gapsIn.m_left)), sc<double>(DISPLAYTOP ? 0 : gapsIn.m_top));
const auto GAPOFFSETBOTTOMRIGHT =
Vector2D(sc<double>(DISPLAYRIGHT ? 0 : (DISPLAYINVERSERIGHT ? 2 * gapsIn.m_right : gapsIn.m_right)), sc<double>(DISPLAYBOTTOM ? 0 : gapsIn.m_bottom));
calcPos = calcPos + GAPOFFSETTOPLEFT + ratioPadding / 2;
calcSize = calcSize - GAPOFFSETTOPLEFT - GAPOFFSETBOTTOMRIGHT - ratioPadding;
if (isPseudo()) {
// Calculate pseudo
float scale = 1;
// adjust if doesn't fit
if (m_pseudoSize.x > calcSize.x || m_pseudoSize.y > calcSize.y) {
if (m_pseudoSize.x > calcSize.x)
scale = calcSize.x / m_pseudoSize.x;
if (m_pseudoSize.y * scale > calcSize.y)
scale = calcSize.y / m_pseudoSize.y;
auto DELTA = calcSize - m_pseudoSize * scale;
calcSize = m_pseudoSize * scale;
calcPos = calcPos + DELTA / 2.f; // center
} else {
auto DELTA = calcSize - m_pseudoSize;
calcPos = calcPos + DELTA / 2.f; // center
calcSize = m_pseudoSize;
}
}
const auto RESERVED = m_window->getFullWindowReservedArea();
calcPos = calcPos + RESERVED.topLeft;
calcSize = calcSize - (RESERVED.topLeft + RESERVED.bottomRight);
Vector2D availableSpace = calcSize;
static auto PCLAMP_TILED = CConfigValue<Hyprlang::INT>("misc:size_limits_tiled");
if (*PCLAMP_TILED) {
const auto borderSize = m_window->getRealBorderSize();
Vector2D monitorAvailable = MONITOR_WORKAREA.size() - Vector2D{2.0 * borderSize, 2.0 * borderSize};
Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}).clamp(Vector2D{0, 0}, monitorAvailable);
Vector2D maxSize = m_window->isFullscreen() ? Vector2D{INFINITY, INFINITY} :
m_window->m_ruleApplicator->maxSize().valueOr(Vector2D{INFINITY, INFINITY}).clamp(Vector2D{0, 0}, monitorAvailable);
calcSize = calcSize.clamp(minSize, maxSize);
calcPos += (availableSpace - calcSize) / 2.0;
calcPos.x = std::clamp(calcPos.x, MONITOR_WORKAREA.x + borderSize, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w - calcSize.x - borderSize);
calcPos.y = std::clamp(calcPos.y, MONITOR_WORKAREA.y + borderSize, MONITOR_WORKAREA.y + MONITOR_WORKAREA.h - calcSize.y - borderSize);
}
if (m_window->onSpecialWorkspace() && !m_window->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
*m_window->m_realPosition = wb.pos();
*m_window->m_realSize = wb.size();
} else {
CBox wb = {calcPos, calcSize};
wb.round(); // avoid rounding mess
*m_window->m_realSize = wb.size();
*m_window->m_realPosition = wb.pos();
}
m_window->updateWindowDecos();
}
void CWindowTarget::assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint) {
if (!space) {
ITarget::assignToSpace(space, focalPoint);
return;
}
// keep the ref here so that moveToWorkspace doesn't unref the workspace
// and assignToSpace doesn't think this is a new target because space wp is dead
const auto WSREF = space->workspace();
m_window->m_monitor = space->workspace()->m_monitor;
m_window->moveToWorkspace(space->workspace());
// layout and various update fns want the target to already have m_workspace set
ITarget::assignToSpace(space, focalPoint);
m_window->updateToplevel();
m_window->updateWindowDecos();
}
bool CWindowTarget::floating() {
return m_window->m_isFloating;
}
void CWindowTarget::setFloating(bool x) {
if (x == m_window->m_isFloating)
return;
m_window->m_isFloating = x;
m_window->m_pinned = false;
m_window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_FLOATING);
}
Vector2D CWindowTarget::clampSizeForDesired(const Vector2D& size) const {
Vector2D newSize = size;
if (const auto m = m_window->minSize(); m)
newSize = newSize.clamp(*m);
if (const auto m = m_window->maxSize(); m)
newSize = newSize.clamp(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}, *m);
return newSize;
}
std::expected<SGeometryRequested, eGeometryFailure> CWindowTarget::desiredGeometry() {
SGeometryRequested requested;
CBox DESIRED_GEOM = g_pXWaylandManager->getGeometryForWindow(m_window.lock());
const auto PMONITOR = m_window->m_monitor.lock();
requested.size = clampSizeForDesired(DESIRED_GEOM.size());
if (m_window->m_isX11) {
Vector2D xy = {DESIRED_GEOM.x, DESIRED_GEOM.y};
xy = g_pXWaylandManager->xwaylandToWaylandCoords(xy);
requested.pos = xy;
}
const auto STOREDSIZE = m_window->m_ruleApplicator->persistentSize().valueOrDefault() ? g_pConfigManager->getStoredFloatingSize(m_window.lock()) : std::nullopt;
if (STOREDSIZE)
requested.size = clampSizeForDesired(*STOREDSIZE);
if (!PMONITOR) {
Log::logger->log(Log::ERR, "{:m} has an invalid monitor in desiredGeometry!", m_window.lock());
return std::unexpected(GEOMETRY_NO_DESIRED);
}
if (DESIRED_GEOM.width <= 2 || DESIRED_GEOM.height <= 2) {
const auto SURFACE = m_window->wlSurface()->resource();
if (SURFACE->m_current.size.x > 5 && SURFACE->m_current.size.y > 5) {
// center on mon and call it a day
requested.pos.reset();
requested.size = clampSizeForDesired(SURFACE->m_current.size);
return requested;
}
if (m_window->m_isX11 && m_window->isX11OverrideRedirect()) {
// check OR windows, they like their shit
const auto SIZE = clampSizeForDesired(m_window->m_xwaylandSurface->m_geometry.w > 0 && m_window->m_xwaylandSurface->m_geometry.h > 0 ?
m_window->m_xwaylandSurface->m_geometry.size() :
Vector2D{600, 400});
if (m_window->m_xwaylandSurface->m_geometry.x != 0 && m_window->m_xwaylandSurface->m_geometry.y != 0) {
requested.size = SIZE;
requested.pos = g_pXWaylandManager->xwaylandToWaylandCoords(m_window->m_xwaylandSurface->m_geometry.pos());
return requested;
}
}
return std::unexpected(m_window->m_isX11 && m_window->isX11OverrideRedirect() ? GEOMETRY_INVALID_DESIRED : GEOMETRY_NO_DESIRED);
}
// TODO: detect a popup in a more consistent way.
if ((DESIRED_GEOM.x == 0 && DESIRED_GEOM.y == 0) || !m_window->m_isX11) {
// middle of parent if available
if (!m_window->m_isX11) {
if (const auto PARENT = m_window->parent(); PARENT) {
const auto POS = PARENT->m_realPosition->goal() + PARENT->m_realSize->goal() / 2.F - DESIRED_GEOM.size() / 2.F;
requested.pos = POS;
}
}
} else {
// if it is, we respect where it wants to put itself, but apply monitor offset if outside
// most of these are popups
Vector2D pos;
if (const auto POPENMON = g_pCompositor->getMonitorFromVector(DESIRED_GEOM.middle()); POPENMON->m_id != PMONITOR->m_id)
pos = Vector2D(DESIRED_GEOM.x, DESIRED_GEOM.y) - POPENMON->m_position + PMONITOR->m_position;
else
pos = Vector2D(DESIRED_GEOM.x, DESIRED_GEOM.y);
requested.pos = pos;
}
if (DESIRED_GEOM.w <= 2 || DESIRED_GEOM.h <= 2)
return std::unexpected(GEOMETRY_NO_DESIRED);
return requested;
}
PHLWINDOW CWindowTarget::window() const {
return m_window.lock();
}
eFullscreenMode CWindowTarget::fullscreenMode() {
return m_window->m_fullscreenState.internal;
}
void CWindowTarget::setFullscreenMode(eFullscreenMode mode) {
if (floating() && m_window->m_fullscreenState.internal == FSMODE_NONE)
rememberFloatingSize(m_box.size());
m_window->m_fullscreenState.internal = mode;
}
std::optional<Vector2D> CWindowTarget::minSize() {
return m_window->minSize();
}
std::optional<Vector2D> CWindowTarget::maxSize() {
return m_window->maxSize();
}
void CWindowTarget::damageEntire() {
g_pHyprRenderer->damageWindow(m_window.lock());
}
void CWindowTarget::warpPositionSize() {
m_window->m_realSize->warp();
m_window->m_realPosition->warp();
m_window->updateWindowDecos();
}
void CWindowTarget::onUpdateSpace() {
if (!space())
return;
m_window->m_monitor = space()->workspace()->m_monitor;
m_window->moveToWorkspace(space()->workspace());
m_window->updateToplevel();
m_window->updateWindowDecos();
}

View file

@ -0,0 +1,40 @@
#pragma once
#include "Target.hpp"
#include "../../desktop/view/Window.hpp"
namespace Layout {
class CWindowTarget : public ITarget {
public:
static SP<ITarget> create(PHLWINDOW w);
virtual ~CWindowTarget() = default;
virtual eTargetType type();
virtual void setPositionGlobal(const CBox& box);
virtual void assignToSpace(const SP<CSpace>& space, std::optional<Vector2D> focalPoint = std::nullopt);
virtual PHLWINDOW window() const;
virtual bool floating();
virtual void setFloating(bool x);
virtual std::expected<SGeometryRequested, eGeometryFailure> desiredGeometry();
virtual eFullscreenMode fullscreenMode();
virtual void setFullscreenMode(eFullscreenMode mode);
virtual std::optional<Vector2D> minSize();
virtual std::optional<Vector2D> maxSize();
virtual void damageEntire();
virtual void warpPositionSize();
virtual void onUpdateSpace();
private:
CWindowTarget(PHLWINDOW w);
Vector2D clampSizeForDesired(const Vector2D& size) const;
void updatePos();
PHLWINDOWREF m_window;
};
};