mirror of
https://github.com/hyprwm/Hyprland.git
synced 2026-08-24 10:04:19 -05:00
layout: allow constraining floating windows to be entirely or partially on-screen (#15492)
* floating: support fitting windows only partially on-screen * floating: add config for whether existing floating windows are kept on-screen * floating: apply "floating windows on-screen" config when moving into layout * floating: add config for whether new floating windows are forced on-screen * tests: (new_)?float_force_onscreen behavior
This commit is contained in:
@@ -1943,3 +1943,71 @@ TEST_CASE(sendFsWindowToAnotherWorkspace) {
|
||||
ASSERT_CONTAINS(str, "floating: 1");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(floatingForceOnscreen) {
|
||||
// Test: move a floating window around
|
||||
NLog::log("{}Testing force-onscreen for existing floating windows", Colors::GREEN);
|
||||
Tests::spawnKitty();
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'on' })"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 100, y = 100, relative = false })"));
|
||||
|
||||
// No constraints
|
||||
OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 0 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -110, y = 0, relative = false })"));
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -110,0");
|
||||
|
||||
// Partially onscreen
|
||||
OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 1 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -110, y = 0, relative = false })"));
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -50,0");
|
||||
|
||||
// Fully onscreen
|
||||
OK(getFromSocket("/eval hl.config({ misc = { float_force_onscreen = 2 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.move({ x = -10, y = 0, relative = false })"));
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: 2,2");
|
||||
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
|
||||
|
||||
// Test: spawn new floating windows offscreen
|
||||
// NOTE: These new-window tests all run with float_force_onscreen = 2.
|
||||
// That's intended, and it shouldn't limit where new windows can spawn.
|
||||
NLog::log("{}Testing force-onscreen for new floating windows", Colors::GREEN);
|
||||
|
||||
// Helper macro, because spawnKitty() doesn't support executing with rules
|
||||
#define WAIT_FOR_WINDOW(N) \
|
||||
do { \
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10)); \
|
||||
int counter = 0; \
|
||||
while (Tests::windowCount() == N) { \
|
||||
counter++; \
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100)); \
|
||||
if (counter > 50) \
|
||||
break; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// No constraints
|
||||
OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 0 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-110, 0} })"));
|
||||
WAIT_FOR_WINDOW(0);
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -110,0");
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
|
||||
|
||||
// Partially onscreen
|
||||
OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 1 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-110, 0} })"));
|
||||
WAIT_FOR_WINDOW(0);
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: -50,0");
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
|
||||
|
||||
// Fully onscreen
|
||||
OK(getFromSocket("/eval hl.config({ misc = { new_float_force_onscreen = 2 }})"));
|
||||
OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true, size = {100, 100}, move = {-10, 0} })"));
|
||||
WAIT_FOR_WINDOW(0);
|
||||
// NOTE: As we spawn with rules, apparently border_size isn't known yet, so
|
||||
// we get placed at (0,0) rather than (2,2)
|
||||
ASSERT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
|
||||
OK(getFromSocket("/dispatch hl.dsp.window.kill()"));
|
||||
|
||||
#undef WAIT_FOR_WINDOW
|
||||
}
|
||||
|
||||
@@ -517,6 +517,8 @@ std::vector<SP<IValue>> Values::getConfigValues() {
|
||||
MS<Bool>("misc:screencopy_force_8b", "forces 8 bit screencopy", true),
|
||||
MS<Bool>("misc:disable_scale_notification", "disables notification popup when a monitor fails to set a suitable scale", false),
|
||||
MS<Bool>("misc:size_limits_tiled", "whether to apply minsize and maxsize rules to tiled windows", false),
|
||||
MS<Int>("misc:new_float_force_onscreen", "whether new floating windows must be placed fully/partially on-screen", 2),
|
||||
MS<Int>("misc:float_force_onscreen", "whether existing floating windows must remain fully/partially on-screen", 0),
|
||||
|
||||
/*
|
||||
* binds:
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../../../target/WindowTarget.hpp"
|
||||
#include "../../../space/Space.hpp"
|
||||
|
||||
#include "../../../../config/ConfigValue.hpp"
|
||||
#include "../../../../Compositor.hpp"
|
||||
#include "../../../../desktop/state/WindowState.hpp"
|
||||
#include "../../../../output/Monitor.hpp"
|
||||
@@ -90,16 +91,20 @@ void CDefaultFloatingAlgorithm::newTarget(SP<ITarget> target) {
|
||||
if (!posOverridden && (!DESIRED_GEOM || !DESIRED_GEOM->pos))
|
||||
windowGeometry = CBox{WORK_AREA.middle() - windowGeometry.size() / 2.F, windowGeometry.size()};
|
||||
|
||||
if (posOverridden // pos is overridden by a rule
|
||||
|| (DESIRED_GEOM && DESIRED_GEOM->pos && target->window() && target->window()->m_isX11) // X11 window with a geom
|
||||
|| WORK_AREA.containsPoint(windowGeometry.middle())) // geometry within work area
|
||||
target->setPositionGlobal(windowGeometry);
|
||||
else {
|
||||
if (!posOverridden // pos is overridden by a rule
|
||||
&& !(DESIRED_GEOM && DESIRED_GEOM->pos && target->window() && target->window()->m_isX11) // X11 window with a geom
|
||||
) {
|
||||
const auto POS = WORK_AREA.middle() - windowGeometry.size() / 2.f;
|
||||
windowGeometry.x = POS.x;
|
||||
windowGeometry.y = POS.y;
|
||||
}
|
||||
|
||||
target->setPositionGlobal(windowGeometry);
|
||||
static auto PFORCEONSCREEN = CConfigValue<Config::INTEGER>("misc:new_float_force_onscreen");
|
||||
switch (*PFORCEONSCREEN) {
|
||||
default:
|
||||
case 0: target->setPositionGlobal(windowGeometry); break;
|
||||
case 1: target->setPositionGlobal(fitBoxInWorkArea(windowGeometry, target, false)); break;
|
||||
case 2: target->setPositionGlobal(fitBoxInWorkArea(windowGeometry, target, true)); break;
|
||||
}
|
||||
|
||||
// TODO: not very OOP, is it?
|
||||
@@ -148,6 +153,8 @@ void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Ve
|
||||
LAST_SIZE = DESIRED ? DESIRED->size : DEFAULT_SIZE;
|
||||
}
|
||||
|
||||
CBox wantPos;
|
||||
|
||||
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)
|
||||
@@ -156,8 +163,8 @@ void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Ve
|
||||
// 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));
|
||||
// put around the current center
|
||||
wantPos = CBox{OLD_CENTER - LAST_SIZE / 2.F, LAST_SIZE};
|
||||
|
||||
} else {
|
||||
// calculate new position
|
||||
@@ -166,26 +173,43 @@ void CDefaultFloatingAlgorithm::movedTarget(SP<ITarget> target, std::optional<Ve
|
||||
const auto MON_FROM_OLD = State::monitorState()->query().vec(OLD_POS).run();
|
||||
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));
|
||||
wantPos = CBox{NEW_POS, LAST_SIZE};
|
||||
}
|
||||
|
||||
setPositionGlobal(target, wantPos);
|
||||
|
||||
updateTarget(target);
|
||||
}
|
||||
|
||||
CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t) {
|
||||
CBox CDefaultFloatingAlgorithm::fitBoxInWorkArea(const CBox& box, SP<ITarget> t, bool fully) {
|
||||
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 (fully) {
|
||||
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.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;
|
||||
if (targetBox.y + targetBox.h > WORK_AREA.y + WORK_AREA.h)
|
||||
targetBox.y = WORK_AREA.y + WORK_AREA.h - targetBox.h;
|
||||
|
||||
} else {
|
||||
// If we'd move offscreen, place centerpoint on nearest edge instead
|
||||
if (targetBox.intersection(WORK_AREA).empty()) {
|
||||
|
||||
targetBox.x = std::max(targetBox.x, WORK_AREA.x - (targetBox.w / 2.F));
|
||||
targetBox.y = std::max(targetBox.y, WORK_AREA.y - (targetBox.h / 2.F));
|
||||
|
||||
if (targetBox.x > WORK_AREA.x + WORK_AREA.w)
|
||||
targetBox.x = WORK_AREA.x + WORK_AREA.w - (targetBox.w / 2.F);
|
||||
|
||||
if (targetBox.y > WORK_AREA.y + WORK_AREA.h)
|
||||
targetBox.y = WORK_AREA.y + WORK_AREA.h - (targetBox.h / 2.F);
|
||||
}
|
||||
}
|
||||
|
||||
return targetBox.addExtents(SBoxExtents{.topLeft = -EXTENTS.topLeft, .bottomRight = -EXTENTS.bottomRight});
|
||||
}
|
||||
@@ -200,7 +224,7 @@ void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> tar
|
||||
pos.w += Δ.x;
|
||||
pos.h += Δ.y;
|
||||
pos.translate(-Δ / 2.F);
|
||||
target->setPositionGlobal(pos);
|
||||
setPositionGlobal(target, pos);
|
||||
|
||||
if (g_layoutManager->dragController()->target() == target)
|
||||
target->warpPositionSize();
|
||||
@@ -211,7 +235,7 @@ void CDefaultFloatingAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> tar
|
||||
void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> target) {
|
||||
auto pos = target->position();
|
||||
pos.translate(Δ);
|
||||
target->setPositionGlobal(pos);
|
||||
setPositionGlobal(target, pos);
|
||||
|
||||
if (g_layoutManager->dragController()->target() == target)
|
||||
target->warpPositionSize();
|
||||
@@ -221,8 +245,8 @@ void CDefaultFloatingAlgorithm::moveTarget(const Vector2D& Δ, SP<ITarget> targe
|
||||
|
||||
void CDefaultFloatingAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
|
||||
auto posABackup = a->position();
|
||||
a->setPositionGlobal(b->position());
|
||||
b->setPositionGlobal(posABackup);
|
||||
setPositionGlobal(a, b->position());
|
||||
setPositionGlobal(b, posABackup);
|
||||
|
||||
updateTarget(a);
|
||||
updateTarget(b);
|
||||
@@ -242,7 +266,7 @@ void CDefaultFloatingAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDire
|
||||
default: Log::logger->log(Log::ERR, "Invalid direction in CDefaultFloatingAlgorithm::moveTargetInDirection"); break;
|
||||
}
|
||||
|
||||
t->setPositionGlobal(pos);
|
||||
setPositionGlobal(t, pos);
|
||||
|
||||
updateTarget(t);
|
||||
}
|
||||
@@ -257,7 +281,7 @@ void CDefaultFloatingAlgorithm::recenter(SP<ITarget> t) {
|
||||
}
|
||||
|
||||
void CDefaultFloatingAlgorithm::setTargetGeom(const CBox& geom, SP<ITarget> target) {
|
||||
target->setPositionGlobal(geom);
|
||||
setPositionGlobal(target, geom);
|
||||
|
||||
updateTarget(target);
|
||||
}
|
||||
@@ -265,3 +289,23 @@ void CDefaultFloatingAlgorithm::setTargetGeom(const CBox& geom, SP<ITarget> targ
|
||||
void CDefaultFloatingAlgorithm::updateTarget(SP<ITarget> t) {
|
||||
m_datas[t] = {.lastBox = t->position()};
|
||||
}
|
||||
|
||||
CBox CDefaultFloatingAlgorithm::setPositionGlobal(SP<ITarget> t, const CBox& box) {
|
||||
CBox adjustedBox;
|
||||
|
||||
static auto PFORCEONSCREEN = CConfigValue<Config::INTEGER>("misc:float_force_onscreen");
|
||||
switch (*PFORCEONSCREEN) {
|
||||
// No no limits, we'll reach for the sky
|
||||
default:
|
||||
case 0: adjustedBox = box.copy(); break;
|
||||
|
||||
// Must be partially visible
|
||||
case 1: adjustedBox = fitBoxInWorkArea(box, t, false); break;
|
||||
|
||||
// Must be fully in-bounds
|
||||
case 2: adjustedBox = fitBoxInWorkArea(box, t, true); break;
|
||||
}
|
||||
|
||||
t->setPositionGlobal(adjustedBox);
|
||||
return adjustedBox;
|
||||
}
|
||||
|
||||
@@ -27,10 +27,12 @@ namespace Layout::Floating {
|
||||
virtual void recenter(SP<ITarget> t);
|
||||
|
||||
private:
|
||||
CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> t);
|
||||
CBox fitBoxInWorkArea(const CBox& box, SP<ITarget> t, bool fully = true);
|
||||
|
||||
void updateTarget(SP<ITarget>);
|
||||
|
||||
CBox setPositionGlobal(SP<ITarget> t, const CBox& box);
|
||||
|
||||
struct SWindowData {
|
||||
CBox lastBox;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user