This commit is contained in:
erstarr 2026-05-06 14:41:57 +02:00 committed by GitHub
commit 82e22ecaff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1071 additions and 50 deletions

View file

@ -195,5 +195,952 @@ TEST_CASE(scrollWindowRule) {
ASSERT(Tests::windowCount(), 2);
// not the greatest test, but as long as res and gaps don't change, we good.
EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 174,1036");
ASSERT_CONTAINS(getFromSocket("/activewindow"), "size: 174,1036");
}
TEST_CASE(testScrollingViewBehaviourDispatchFocusWindowFollowFocusFalse) {
/*
focuswindow DOES NOT move the scrolling view when follow_focus = false
---------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: focuswindow dispatch SHOULD NOT move scrolling view when follow_focus = false", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("Could not spawn kitty with win class `a`");
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("Could not spawn kitty with win class `b`");
}
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// if the view does not move, we expect the x coordinate of the window of class "a" to be negative, as it would be to the left of the viewport
const std::string posA = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const int posAx = std::stoi(posA.substr(0, posA.find(',')));
if (posAx < 0) {
NLog::log("{}Passed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors::GREEN, Colors::RESET, posAx);
} else {
FAIL_TEST("{}Failed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors::RED, Colors::RESET, posAx);
}
// clean up
// revert the changes made to config
NLog::log("{}Restoring config state", Colors::YELLOW);
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourDispatchFocusWindowFollowFocustrue) {
/*
focuswindow DOES move the view when follow_focus = true
--------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: focuswindow dispatch SHOULD move scrolling view when follow_focus = true", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("Could not spawn kitty with win class `a`");
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("Could not spawn kitty with win class `b`");
}
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// If the view does not move, we expect the x coordinate of the window of class "a" to be negative, as it would be to the left of the viewport.
// If it is not, the view moved, which is what we expect to happen.
const std::string posA = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const int posAx = std::stoi(posA.substr(0, posA.find(',')));
if (posAx < 0) {
FAIL_TEST("{}Failed: {}Expected the x coordinate of window of class \"a\" to be >= 0, got {}.", Colors::RED, Colors::RESET, posAx);
} else {
NLog::log("{}Passed: {}Expected the x coordinate of window of class \"a\" to be >= 0, got {}.", Colors::GREEN, Colors::RESET, posAx);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourFocusFallback) {
/*
Focus fallback from killing a floating window onto a tiled window must NOT move scrolling view, regardless of follow_focus
--------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: focus fallback from floating window to a tiled window should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with class `c`", Colors::RED);
}
// make it (window of class:c) float - the view now mush have shifted to fit window class:b
OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:c'})"));
// establish focus history
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})"));
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})"));
// kill the floating window
// Expect the focus to fall back to the left tiled window
OK(getFromSocket("/dispatch hl.dsp.window.kill({window = 'class:c'})"));
Tests::waitUntilWindowsN(2);
// The focus now must have fallen back to tiled window of class "a".
// If the view did not move, we expect currently focused window's (class:a) to have "at: " x coordinat value <0 (must be left of the viewport)
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourFocusFallbackWithGroups) {
// same idea as testScrollingViewBehaviourFocusFallback, but with window of class "a" being grouped.
NLog::log("{}Testing scrolling view behaviour: focus fallback from floating window to a grouped tiled should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
// to correctly set up windows for the test
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
// only one tiled window will be grouped for the test
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
// make it a grouped. There need not be any other windows in the group for this test
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with class `c`", Colors::RED);
}
// make it float - the view now mush have shifted to fit window class:b
OK(getFromSocket("/dispatch hl.dsp.window.float({action = 'enable', window = 'class:c'})"));
// establish focus history
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})"));
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:c'})"));
// kill the floating window
OK(getFromSocket("/dispatch hl.dsp.window.kill({window = 'class:c'})"));
Tests::waitUntilWindowsN(2);
// The focus now must have fallen back to tiled window of class "a".
// If the view did not move, we expect currently focused window's (class:a) to have "at: " x coordinat value <0 (must be left of the viewport)
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}Expected the x coordinate of window of class \"a\" to be < 0, got {}.", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourWorkspaceChange) {
/*
When you change to a scrolling workspace, the focused window in that workspace must not be pulled into view, regardless of follow_focus
---------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: changing to a scrolling workspace should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
// switch to workspace 1 for this test
OK(getFromSocket("/dispatch hl.dsp.focus({workspace = '1'})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// does not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// change to workspace 2, then back to workspace 1 again
OK(getFromSocket("/dispatch hl.dsp.focus({workspace = '2'})"));
OK(getFromSocket("/dispatch hl.dsp.focus({workspace = '1'})"));
// If the scrolling view did not move, the x value for `at:` of the currently focused window, class:a, must be <0 (must be left of the viewport)
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourSpecialWorkspaceChange) {
/*
When you change to a special scrolling workspace from a normal workspace, the focused window in that workspace must not be pulled into view, regardless of follow_focus
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: changing to a special scrolling workspace from a normal workspace should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
// We'll test in this special workspace
OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('name:scroll_S')"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// does not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// change to workspace 2, then back to special "scroll_S" workspace again
OK(getFromSocket("/dispatch hl.dsp.focus({workspace = '2'})"));
OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('name:scroll_S')"));
// Reestablish focus since it is finnicky in hyprtester - Harmless and does not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// If the scrolling view did not move, the x value for `at:` of the currently focused windows, class:c, must be <0 (must be left of the viewport)
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourSpecialToSpecialWorkspaceChange) {
/*
We also test switching between 2 special workspaces
This follows the same idea and dependencies as the test testScrollingViewBehaviourSpecialWorkspaceChange()
*/
NLog::log("{}Testing scrolling view behaviour: changing to a special scrolling workspace from another special workspace should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
// We'll test in this special workspace
OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('name:scroll_S')"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// does not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// change to special workspace "scroll_F", then back to special "scroll_S" workspace again
OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('name:scroll_F')"));
OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('name:scroll_S')"));
// Reestablish focus since it is finnicky in hyprtester - Harmless and does not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// If the scrolling view did not move, the x value for `at:` of the currently focused windows, class:c, must be <0 (must be left of the viewport)
const std::string currentWindowPosSPECIAL = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosSPECIALX = currentWindowPosSPECIAL.substr(0, currentWindowPosSPECIAL.find(','));
// test pass
if (std::stoi(currentWindowPosSPECIALX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosSPECIALX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosSPECIALX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourCloseWindowInGroup) {
/*
When you change close a window inside a group (NOT destroying the group!), it should not cause scrolling view to shift to pull that group into view, regardless of follow_focus
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: closing a window in a group (> 1 window in group) should not move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
// this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
// We need 2 windows to be grouped, the third one not.
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with win class `c`", Colors::RED);
}
// switch focus to group. This will not move view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})"));
// kill window class:b. we expect that this should cause not difference in the position of the group
OK(getFromSocket("/dispatch hl.dsp.window.kill({window = 'class:b'})"));
Tests::waitUntilWindowsN(2);
// If the scrolling view did not move, the x value for `at:` of the currently focused windows, class:a, must be <0 (must be left of the viewport)
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveWindowIntoGroupFollowFocusFalse) {
/*
when a window is moved inside a group, scrolling view should not move to fit that group when follow_focus = false
-----------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: moving a window into a group SHOULD NOT move scrolling view if follow_focus = 0", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with win class `c`", Colors::RED);
}
// focus class:b
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})"));
// move it into the group where class:a is
OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));
// the focus now should still be on class:b window. If the view did not move, its x coordinate for its `at:` value should be <0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'b' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'b' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveWindowInGroupFollowFocusTrue) {
/*
when a window is moved inside a group, scrolling view should move to fit that group when follow_focus = true
------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: moving a window in a group SHOULD move scrolling view if follow_focus = true", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with win class `c`", Colors::RED);
}
// focus class:b
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})"));
// move it into the group where class:a is
OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));
// the focus now should still be on class:b window. If the scrolling view did move, its x coordinate for its `at:` value should be >= 0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test fail
if (std::stoi(currentWindowPosX) < 0) {
FAIL_TEST("{}window of class 'b' does not have x coordinates >= 0 for its position: {}", Colors::RED, currentWindowPosX);
}
// test pass
else {
NLog ::log("{}Passed: {}window of class 'b' has x coordinates >= 0 for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourNewLayer) {
/*
Starting a program on a different layer shouldn't cause scrolling view to move to fit the window that was focused when this program was started, regardless of follow_focus
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: new program occupying another layer shouldn't move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// focus class:a - this does not move scrolling view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
NLog::log("{}Spawning kitty layer {}", Colors::YELLOW, "myLayer");
if (!Tests::spawnLayerKitty("myLayer")) {
FAIL_TEST("{}Error: {} layer did not spawn", Colors::RED, "myLayer");
}
// If the scrolling view did not move, class:a window's x coordinate for its `at:` value should be <0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all layers
NLog::log("{}Killing all layers", Colors::YELLOW);
Tests::killAllLayers();
ASSERT(Tests::layerCount(), 0);
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMaximise) {
/*
maximising and then unmaximising a window shouldn't move scrolling view, regardless of follow_focus
---------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: maximising and then unmaximising a window shouldn't move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// focus class:a - this does not move scrolling view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// fullscreen class:a window
OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized', action = 'set', window = 'class:a'})"));
// unfullscreen class:a window
OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'maximized', action = 'unset', window = 'class:a'})"));
// If the scrolling view did not move, class:a window's x coordinate for its `at:` value should be <0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourFullscreen) {
/*
This is almost the same as the testScrollingViewBehaviourMaximise() test, just with fullscreen 1 (fullscreen) instead of fullscreen 0 (maximise)
fullscreening and then unfullscreening a window shouldn't move scrolling view, regardless of follow_focus
*/
NLog::log("{}Testing scrolling view behaviour: fullscreening and then unfullscreening a window shouldn't move scrolling view", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test - this is to avoid unwanted view shifts when setting up the windows
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// focus class:a - this does not move scrolling view when follow_focus = 0
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:a'})"));
// maximise class:a window
OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen', action = 'set', window = 'class:a'})"));
// unmaximise class:a window
OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({mode = 'fullscreen', action = 'unset', window = 'class:a'})"));
// If the scrolling view did not move, class:a window's x coordinate for its `at:` value should be <0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveFocusFollowFocusFalse) {
/*
dispatching movefocus when follow_focus = false should not cause scrolling view to move
---------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: movefocus does not cause scrolling view to move if follow_focus = false", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// we expect that after dispatching this, scrolling view must not have moved
OK(getFromSocket("/dispatch hl.dsp.focus({direction = 'left'})"));
// If the scrolling view did not move, class:a window's x coordinate for its `at:` value should be < 0.
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveFocusFollowFocusTrue) {
/*
dispatching movefocus when follow_focus = true should cause scrolling view to move
----------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: movefocus does cause scrolling view to move if follow_focus = true", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
// we expect that after dispatching this, scrolling view must have moved since follow_focus = true
OK(getFromSocket("/dispatch hl.dsp.focus({direction = 'left'})"));
// If the scrolling view moved, class:a window's x coordinate for its `at:` value should be >= 0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test fail
if (std::stoi(currentWindowPosX) < 0) {
FAIL_TEST("{}Failed: {}window of class 'a' does not have x coordinates >= 0 for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// test pass
else {
NLog ::log("{}Passed: {}window of class 'a' has x coordinates >= 0 for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveFocusInGroupFollowFocusFalse) {
/*
When movefocus is dispatched within groups to move focus from one group member to another, scrolling view must not move if follow_focus = false
-----------------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: movefocus within groups does not cause scrolling view to move if follow_focus = false", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
// necessary to make sure movefocus first cycles through tabs in a group
OK(getFromSocket("/eval hl.config({ binds = {movefocus_cycles_groupfirst = true}})"));
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
OK(getFromSocket("/eval hl.config({scrolling = {follow_focus = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with win class `c`", Colors::RED);
}
// focus class:b. This does not cause scrolling view to move when follow_focus = false
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})"));
// move it into the group where class:a is. This does not cause scrolling view to move when follow_focus = false
OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));
// we move from one window of a group to another (from class:b to class:a) via movefocus
OK(getFromSocket("/dispatch hl.dsp.focus({direction = 'left'})"));
// the focus now should still be on class:a window. If the scrolling view did not move, its x coordinate for its `at:` value should be < 0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test pass
if (std::stoi(currentWindowPosX) < 0) {
NLog ::log("{}Passed: {}window of class 'a' has negative x coordinates for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// test fail
else {
FAIL_TEST("{}Failed: {}window of class 'a' does not have negative x coordinates for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}
TEST_CASE(testScrollingViewBehaviourMoveFocusInGroupFollowFocusTrue) {
/*
When movefocus is dispatched within groups to move focus from one group member to another, scrolling view must move if follow_focus = true
------------------------------------------------------------------------------------------------------------------------------------------
*/
NLog::log("{}Testing scrolling view behaviour: movefocus within groups does causes scrolling view to move if follow_focus = true", Colors::GREEN);
OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })"));
// ensure variables are correctly set for the test
// necessary to make sure movefocus first cycles through tabs in a group
OK(getFromSocket("/eval hl.config({ binds = {movefocus_cycles_groupfirst = true}})"));
OK(getFromSocket("/eval hl.config({group = {auto_group = false}})"));
if (!Tests::spawnKitty("a")) {
FAIL_TEST("{}Failed to spawn kitty with win class `a`", Colors::RED);
}
OK(getFromSocket("/dispatch hl.dsp.group.toggle({window = 'class:a'})"));
OK(getFromSocket("/dispatch hl.dsp.layout('colresize 0.8')"));
if (!Tests::spawnKitty("b")) {
FAIL_TEST("{}Failed to spawn kitty with win class `b`", Colors::RED);
}
if (!Tests::spawnKitty("c")) {
FAIL_TEST("{}Failed to spawn kitty with win class `c`", Colors::RED);
}
// focus class:b. This does not cause scrolling view to move when follow_focus = false
OK(getFromSocket("/dispatch hl.dsp.focus({window = 'class:b'})"));
// move it into the group where class:a is. This does not cause scrolling view to move when follow_focus = false
OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })"));
// we move from one window of a group to another (from class:b to class:a) via movefocus
OK(getFromSocket("/dispatch hl.dsp.focus({direction = 'left'})"));
// the focus now should still be on class:a window. If the scrolling view moved, its x coordinate for its `at:` value should be >= 0
const std::string currentWindowPos = Tests::getAttribute(getFromSocket("/activewindow"), "at");
const std::string currentWindowPosX = currentWindowPos.substr(0, currentWindowPos.find(','));
// test fail
if (std::stoi(currentWindowPosX) < 0) {
FAIL_TEST("{}Failed: {}window of class 'a' does not have x coordinates >= 0 for its position: {}", Colors::RED, Colors::RESET, currentWindowPosX);
}
// test pass
else {
NLog ::log("{}Passed: {}window of class 'a' has x coordinates >= 0 for its position: {}", Colors ::GREEN, Colors::RESET, currentWindowPosX);
}
// clean up
// kill all windows
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
ASSERT(Tests::windowCount(), 0);
}

View file

@ -2259,7 +2259,7 @@ void CCompositor::setWindowFullscreenState(const PHLWINDOW PWINDOW, Desktop::Vie
Desktop::Rule::RULE_PROP_FULLSCREENSTATE_INTERNAL | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
PWINDOW->updateDecorationValues();
g_layoutManager->recalculateMonitor(PMONITOR);
g_layoutManager->recalculateMonitor(PMONITOR, Layout::CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_FULLSCREEN);
// make all windows and layers on the same workspace under the fullscreen window
for (auto const& w : m_windows) {

View file

@ -60,10 +60,10 @@ static void switchToWindow(PHLWINDOW PWINDOWTOCHANGETO, bool forceFSCycle = fals
g_pInputManager->unconstrainMouse();
if (PLASTWINDOW && PLASTWINDOW->m_workspace == PWINDOWTOCHANGETO->m_workspace && PLASTWINDOW->isFullscreen())
Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle);
Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_SWITCH_TO_WINDOW_HARD, nullptr, forceFSCycle);
else {
updateRelativeCursorCoords();
Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle);
Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_SWITCH_TO_WINDOW_SOFT, nullptr, forceFSCycle);
PWINDOWTOCHANGETO->warpCursor();
if (*PNOWARPS == 0 || *PFOLLOWMOUSE < 2) {
@ -430,7 +430,7 @@ ActionResult Actions::focus(PHLWINDOW window) {
Desktop::focusState()->monitor()->m_activeSpecialWorkspace != window->m_workspace) // NOLINTNEXTLINE
Actions::changeWorkspace(PWORKSPACE);
Desktop::focusState()->fullWindowFocus(window, Desktop::FOCUS_REASON_KEYBIND, nullptr, false);
Desktop::focusState()->fullWindowFocus(window, Desktop::FOCUS_REASON_DISPATCH_FOCUSWINDOW, nullptr, false);
window->warpCursor();
return {};
@ -1269,7 +1269,7 @@ static void moveWindowIntoGroupHelper(PHLWINDOW pWindow, PHLWINDOW pWindowInDire
pWindowInDirection->m_group->add(pWindow);
pWindowInDirection->m_group->setCurrent(pWindow);
pWindow->updateWindowDecos();
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND);
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_DISPATCH_MOVEWINDOWINTOGROUP);
pWindow->warpCursor();
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveintogroup", .data = std::format("{:x}", rc<uintptr_t>(pWindow.get()))});

View file

@ -305,5 +305,6 @@ void CFocusState::resetWindowFocus() {
}
bool Desktop::isHardInputFocusReason(eFocusReason r) {
return r == FOCUS_REASON_NEW_WINDOW || r == FOCUS_REASON_KEYBIND || r == FOCUS_REASON_GHOSTS || r == FOCUS_REASON_CLICK || r == FOCUS_REASON_DESKTOP_STATE_CHANGE;
return r == FOCUS_REASON_NEW_WINDOW || r == FOCUS_REASON_KEYBIND || r == FOCUS_REASON_GHOSTS || r == FOCUS_REASON_CLICK || r == FOCUS_REASON_DESKTOP_STATE_CHANGE ||
r == FOCUS_REASON_UNMAP_WINDOW_TILING || r == FOCUS_REASON_SWITCH_TO_WINDOW_HARD;
}

View file

@ -10,9 +10,19 @@ namespace Desktop {
FOCUS_REASON_UNKNOWN = 0,
FOCUS_REASON_FFM,
FOCUS_REASON_KEYBIND,
FOCUS_REASON_DISPATCH_FOCUSWINDOW,
FOCUS_REASON_DISPATCH_MOVEWINDOWINTOGROUP,
FOCUS_REASON_CLICK,
FOCUS_REASON_OTHER,
FOCUS_REASON_DESKTOP_STATE_CHANGE,
FOCUS_REASON_SWITCH_TO_WINDOW_SOFT,
FOCUS_REASON_SWITCH_TO_WINDOW_HARD,
FOCUS_REASON_GROUP_CURRENT_WINDOW_CHANGE,
FOCUS_REASON_WORKSPACE_CHANGE,
FOCUS_REASON_TOGGLE_SPECIAL_WORKSPACE,
FOCUS_REASON_UNMAP_WINDOW_TILING,
FOCUS_REASON_UNMAP_WINDOW_FLOATING,
FOCUS_REASON_UNMAP_GROUPED_WINDOW,
FOCUS_REASON_NEW_WINDOW,
FOCUS_REASON_GHOSTS,
};

View file

@ -122,7 +122,7 @@ void CGroup::add(PHLWINDOW w) {
m_target->recalc();
}
void CGroup::remove(PHLWINDOW w, Math::eDirection dir) {
void CGroup::remove(PHLWINDOW w, Math::eDirection dir, eRemoveFromGroupReason reason) {
std::optional<size_t> idx;
for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows.at(i) == w) {
@ -171,6 +171,10 @@ void CGroup::remove(PHLWINDOW w, Math::eDirection dir) {
default: break;
}
}
// We don't need to assign a window to a new space if we intend to unmap it
if (reason == REMOVE_FROM_GROUP_REASON_UNMAP_WINDOW)
return;
w->m_target->assignToSpace(m_target->space(), focalPoint);
}
}
@ -215,7 +219,7 @@ void CGroup::setCurrent(size_t idx) {
}
if (WASFOCUS)
Desktop::focusState()->rawWindowFocus(current(), FOCUS_REASON_DESKTOP_STATE_CHANGE);
Desktop::focusState()->rawWindowFocus(current(), FOCUS_REASON_GROUP_CURRENT_WINDOW_CHANGE);
}
void CGroup::setCurrent(PHLWINDOW w) {

View file

@ -15,10 +15,15 @@ namespace Desktop::View {
static SP<CGroup> create(std::vector<PHLWINDOWREF>&& windows);
~CGroup();
enum eRemoveFromGroupReason : uint8_t {
REMOVE_FROM_GROUP_REASON_UNKNOWN, // when the remove from group reason is unknown or not important to preserve
REMOVE_FROM_GROUP_REASON_UNMAP_WINDOW,
};
bool has(PHLWINDOW w) const;
void add(PHLWINDOW w);
void remove(PHLWINDOW w, Math::eDirection dir = Math::DIRECTION_DEFAULT);
void remove(PHLWINDOW w, Math::eDirection dir = Math::DIRECTION_DEFAULT, eRemoveFromGroupReason reason = REMOVE_FROM_GROUP_REASON_UNKNOWN);
void moveCurrent(bool next);
void setCurrent(size_t idx);
void setCurrent(PHLWINDOW w);

View file

@ -2361,7 +2361,7 @@ void CWindow::unmapWindow() {
PWORKSPACE->m_hasFullscreenWindow = false;
if (m_group)
m_group->remove(m_self.lock());
m_group->remove(m_self.lock(), Math::DIRECTION_DEFAULT, CGroup::REMOVE_FROM_GROUP_REASON_UNMAP_WINDOW);
g_layoutManager->removeTarget(m_target);
@ -2390,9 +2390,9 @@ void CWindow::unmapWindow() {
if (candidate != Desktop::focusState()->window() && candidate) {
if (candidate == nextInGroup)
Desktop::focusState()->rawWindowFocus(candidate, FOCUS_REASON_DESKTOP_STATE_CHANGE);
Desktop::focusState()->rawWindowFocus(candidate, FOCUS_REASON_UNMAP_GROUPED_WINDOW);
else
Desktop::focusState()->fullWindowFocus(candidate, FOCUS_REASON_DESKTOP_STATE_CHANGE);
Desktop::focusState()->fullWindowFocus(candidate, m_self->m_isFloating ? FOCUS_REASON_UNMAP_WINDOW_FLOATING : FOCUS_REASON_UNMAP_WINDOW_TILING);
if ((*PEXITRETAINSFS || candidate == nextInGroup) && CURRENTWINDOWFSSTATE)
g_pCompositor->setWindowFullscreenInternal(candidate, CURRENTFSMODE);

View file

@ -1409,13 +1409,13 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo
pWindow = pWorkspace->getFocusCandidate();
}
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND);
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_WORKSPACE_CHANGE);
}
if (!noMouseMove)
g_pInputManager->simulateMouseMovement();
g_layoutManager->recalculateMonitor(m_self.lock());
g_layoutManager->recalculateMonitor(m_self.lock(), Layout::CLayoutManager::RECALCULATE_MONITOR_REASON_WORKSPACE_CHANGE);
g_pEventManager->postEvent(SHyprIPCEvent{"workspace", pWorkspace->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"workspacev2", std::format("{},{}", pWorkspace->m_id, pWorkspace->m_name)});
@ -1478,11 +1478,11 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
if (POLDSPECIAL)
POLDSPECIAL->m_events.activeChanged.emit();
g_layoutManager->recalculateMonitor(m_self.lock());
g_layoutManager->recalculateMonitor(m_self.lock(), Layout::CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_SPECIAL_WORKSPACE);
if (!(Desktop::focusState()->window() && Desktop::focusState()->window()->m_pinned && Desktop::focusState()->window()->m_monitor == m_self)) {
if (const auto PLAST = m_activeWorkspace->getLastFocusedWindow(); PLAST)
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND);
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_TOGGLE_SPECIAL_WORKSPACE);
else
g_pInputManager->refocus();
}
@ -1508,7 +1508,7 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
if (PMONITOR && PMONITOR->m_activeSpecialWorkspace == pWorkspace) {
PMONITOR->m_activeSpecialWorkspace.reset();
g_layoutManager->recalculateMonitor(PMONITOR);
g_layoutManager->recalculateMonitor(PMONITOR, Layout::CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_SPECIAL_WORKSPACE);
g_pHyprRenderer->damageMonitor(PMONITOR);
g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + PMONITOR->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"activespecialv2", ",," + PMONITOR->m_name});
@ -1573,11 +1573,11 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
}
}
g_layoutManager->recalculateMonitor(m_self.lock());
g_layoutManager->recalculateMonitor(m_self.lock(), Layout::CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_SPECIAL_WORKSPACE);
if (!(Desktop::focusState()->window() && Desktop::focusState()->window()->m_pinned && Desktop::focusState()->window()->m_monitor == m_self)) {
if (const auto PLAST = pWorkspace->getLastFocusedWindow(); PLAST)
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND);
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_TOGGLE_SPECIAL_WORKSPACE);
else
g_pInputManager->refocus();
}

View file

@ -332,18 +332,19 @@ void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP<I
sourceSize = {sourceX.end - sourceX.start, sourceY.end - sourceY.start};
}
void CLayoutManager::recalculateMonitor(PHLMONITOR m) {
void CLayoutManager::recalculateMonitor(PHLMONITOR m, eRecalculateMonitorReason reason) {
if (m->m_activeSpecialWorkspace)
m->m_activeSpecialWorkspace->m_space->recalculate();
m->m_activeSpecialWorkspace->m_space->recalculate(recalcMonitorReasonToRecalcReason(reason));
if (m->m_activeWorkspace)
m->m_activeWorkspace->m_space->recalculate();
m->m_activeWorkspace->m_space->recalculate(recalcMonitorReasonToRecalcReason(reason));
}
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();
ws->m_space->recalculate(RECALCULATE_REASON_INVALIDATE_MONITOR_GEOMETRIES);
}
}
}

View file

@ -45,6 +45,13 @@ namespace Layout {
CLayoutManager();
~CLayoutManager() = default;
enum eRecalculateMonitorReason : uint8_t {
RECALCULATE_MONITOR_REASON_UNKNOWN, // when the recalculate monitor reason is unknown or not important to preserve
RECALCULATE_MONITOR_REASON_WORKSPACE_CHANGE,
RECALCULATE_MONITOR_REASON_TOGGLE_SPECIAL_WORKSPACE,
RECALCULATE_MONITOR_REASON_TOGGLE_FULLSCREEN,
};
void newTarget(SP<ITarget> target, SP<CSpace> space);
void removeTarget(SP<ITarget> target);
@ -76,7 +83,7 @@ namespace Layout {
void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP<ITarget> target, eMouseBindMode mode, int corner, const Vector2D& beginSize);
void invalidateMonitorGeometries(PHLMONITOR);
void recalculateMonitor(PHLMONITOR);
void recalculateMonitor(PHLMONITOR, eRecalculateMonitorReason reason = RECALCULATE_MONITOR_REASON_UNKNOWN);
const UP<Supplementary::CDragStateController>& dragController();

View file

@ -101,9 +101,9 @@ size_t CAlgorithm::floatingTargets() const {
return m_floatingTargets.size();
}
void CAlgorithm::recalculate() {
m_tiled->recalculate();
m_floating->recalculate();
void CAlgorithm::recalculate(eRecalculateReason reason) {
m_tiled->recalculate(reason);
m_floating->recalculate(reason);
const auto PWORKSPACE = m_space->workspace();
if (!PWORKSPACE)

View file

@ -5,6 +5,7 @@
#include "../../helpers/memory/Memory.hpp"
#include "../LayoutManager.hpp"
#include "../space/Space.hpp"
#include <expected>
#include <optional>
@ -34,7 +35,7 @@ namespace Layout {
Config::ErrorResult layoutMsg(const std::string_view& sv);
std::optional<Vector2D> predictSizeForNewTiledTarget();
void recalculate();
void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
void recenter(SP<ITarget> t);
void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);

View file

@ -4,7 +4,7 @@
using namespace Layout;
void IFloatingAlgorithm::recalculate() {
void IFloatingAlgorithm::recalculate(eRecalculateReason reason) {
;
}

View file

@ -22,7 +22,7 @@ namespace Layout {
virtual void recenter(SP<ITarget> t);
virtual void recalculate();
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
protected:
IFloatingAlgorithm() = default;

View file

@ -5,6 +5,7 @@
#include "../../helpers/memory/Memory.hpp"
#include "../LayoutManager.hpp"
#include "../space/Space.hpp"
#include <expected>
@ -30,7 +31,7 @@ namespace Layout {
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE) = 0;
// recalculate layout
virtual void recalculate() = 0;
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN) = 0;
// swap targets
virtual void swapTargets(SP<ITarget> a, SP<ITarget> b) = 0;

View file

@ -488,7 +488,7 @@ void CDwindleAlgorithm::swapTargets(SP<ITarget> a, SP<ITarget> b) {
nodeB->pTarget = a;
}
void CDwindleAlgorithm::recalculate() {
void CDwindleAlgorithm::recalculate(eRecalculateReason reason) {
calculateWorkspace();
}

View file

@ -35,7 +35,7 @@ namespace Layout::Tiled {
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);

View file

@ -424,7 +424,7 @@ void CMasterAlgorithm::moveTargetInDirection(SP<ITarget> t, Math::eDirection dir
}
}
void CMasterAlgorithm::recalculate() {
void CMasterAlgorithm::recalculate(eRecalculateReason reason) {
calculateWorkspace();
}

View file

@ -53,7 +53,7 @@ namespace Layout::Tiled {
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);

View file

@ -125,7 +125,7 @@ void CMonocleAlgorithm::resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRe
// monocle layout doesn't support manual resizing, all windows are fullscreen
}
void CMonocleAlgorithm::recalculate() {
void CMonocleAlgorithm::recalculate(eRecalculateReason reason) {
if (m_targetDatas.empty())
return;

View file

@ -26,7 +26,7 @@ namespace Layout::Tiled {
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);

View file

@ -535,7 +535,16 @@ CScrollingAlgorithm::CScrollingAlgorithm() {
if (!TARGET || TARGET->floating())
return;
focusOnInput(TARGET, reason == Desktop::FOCUS_REASON_CLICK ? INPUT_MODE_CLICK : (Desktop::isHardInputFocusReason(reason) ? INPUT_MODE_KB : INPUT_MODE_SOFT));
// if follow_focus != 0, focuswindow always moves scrolling view
// if follow_focus != 0, change in a group's current window state always moves scrolling view
// if follow_focus != 0, moving a window into group via the corresponding dispatches `moveintogroup`, `movewindoworgroup` always moves scrolling view
// if follow_focus != 0, moving focus via dispatches that cause switching to a specific window via calling switchToWindow(), such as movefocus, cyclenext, focuscurrentor(last/urgent); always moves scrolling view
if (*PFOLLOW_FOCUS &&
(reason == Desktop::FOCUS_REASON_DISPATCH_FOCUSWINDOW || reason == Desktop::FOCUS_REASON_GROUP_CURRENT_WINDOW_CHANGE ||
reason == Desktop::FOCUS_REASON_DISPATCH_MOVEWINDOWINTOGROUP || reason == Desktop::FOCUS_REASON_SWITCH_TO_WINDOW_SOFT))
focusOnInput(TARGET, INPUT_MODE_HARD);
else
focusOnInput(TARGET, reason == Desktop::FOCUS_REASON_CLICK ? INPUT_MODE_CLICK : (Desktop::isHardInputFocusReason(reason) ? INPUT_MODE_HARD : INPUT_MODE_SOFT));
});
// Initialize default widths and direction
@ -576,7 +585,7 @@ void CScrollingAlgorithm::focusOnInput(SP<ITarget> target, eInputMode input) {
}
// if we moved via non-kb, and it's fully visible, ignore
if (m_scrollingData->visible(TARGETDATA->column.lock(), true) && input != INPUT_MODE_KB)
if (m_scrollingData->visible(TARGETDATA->column.lock(), true) && input != INPUT_MODE_HARD)
return;
static const auto PFITMETHOD = CConfigValue<Config::INTEGER>("scrolling:focus_fit_method");
@ -796,7 +805,7 @@ void CScrollingAlgorithm::resizeTarget(const Vector2D& delta, SP<ITarget> target
m_scrollingData->recalculate(true);
}
void CScrollingAlgorithm::recalculate() {
void CScrollingAlgorithm::recalculate(eRecalculateReason reason) {
// guard against recalculation during transitional monitor states
// (e.g. monitor reconnecting after suspend where workspace/monitor may not be ready)
if (!m_parent || !m_parent->space() || !m_parent->space()->workspace() || !m_parent->space()->workspace()->m_monitor)
@ -807,8 +816,14 @@ void CScrollingAlgorithm::recalculate() {
const auto TARGETDATA = dataFor(TARGET);
if (TARGETDATA && !m_scrollingData->visible(TARGETDATA->column.lock(), true))
focusOnInput(Desktop::focusState()->window()->layoutTarget(), INPUT_MODE_KB);
if (TARGETDATA && !m_scrollingData->visible(TARGETDATA->column.lock(), true)) {
/* guard against unwanted scrolling viewport moves - If recalculate() was called, it is assumed that either the INPUT_MODE will be HARD (i.e. it is meant to move the scrolling viewport) or
it is not meant to move the scrolling viewport.
(e.g. changing workspace to a scrolling layout workspace fits the focused window in that workspace into view) */
if (Layout::isHardRecalculateReason(reason))
focusOnInput(Desktop::focusState()->window()->layoutTarget(), INPUT_MODE_HARD);
}
}
m_scrollingData->recalculate();

View file

@ -99,7 +99,7 @@ namespace Layout::Tiled {
virtual void removeTarget(SP<ITarget> target);
virtual void resizeTarget(const Vector2D& Δ, SP<ITarget> target, eRectCorner corner = CORNER_NONE);
virtual void recalculate();
virtual void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
virtual SP<ITarget> getNextCandidate(SP<ITarget> old);
@ -115,7 +115,7 @@ namespace Layout::Tiled {
enum eInputMode : uint8_t {
INPUT_MODE_SOFT = 0,
INPUT_MODE_CLICK,
INPUT_MODE_KB
INPUT_MODE_HARD
};
private:

View file

@ -146,11 +146,11 @@ SP<CAlgorithm> CSpace::algorithm() const {
return m_algorithm;
}
void CSpace::recalculate() {
void CSpace::recalculate(eRecalculateReason reason) {
recheckWorkArea();
if (m_algorithm)
m_algorithm->recalculate();
m_algorithm->recalculate(reason);
}
void CSpace::setFullscreen(SP<ITarget> t, eFullscreenMode mode) {
@ -159,7 +159,7 @@ void CSpace::setFullscreen(SP<ITarget> t, eFullscreenMode mode) {
if (mode == FSMODE_NONE && m_algorithm && t->floating())
m_algorithm->recenter(t);
recalculate();
recalculate(RECALCULATE_REASON_TOGGLE_FULLSCREEN);
}
Config::ErrorResult CSpace::layoutMsg(const std::string_view& sv) {
@ -202,6 +202,21 @@ SP<ITarget> CSpace::getNextCandidate(SP<ITarget> old) {
return !m_algorithm ? nullptr : m_algorithm->getNextCandidate(old);
}
bool Layout::isHardRecalculateReason(eRecalculateReason reason) {
return reason != RECALCULATE_REASON_WORKSPACE_CHANGE && reason != RECALCULATE_REASON_SPECIAL_WORKSPACE_TOGGLE && reason != RECALCULATE_REASON_TOGGLE_FULLSCREEN &&
reason != RECALCULATE_REASON_INVALIDATE_MONITOR_GEOMETRIES && reason != RECALCULATE_REASON_RENDER_MOINTOR;
}
const std::vector<WP<ITarget>>& CSpace::targets() const {
return m_targets;
}
eRecalculateReason Layout::recalcMonitorReasonToRecalcReason(CLayoutManager::eRecalculateMonitorReason reason) {
// If eRecalculateMonitorReason doesn't have a eRecalculateReason pair, it'll return nullopt
switch (reason) {
case CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_SPECIAL_WORKSPACE: return RECALCULATE_REASON_SPECIAL_WORKSPACE_TOGGLE;
case CLayoutManager::RECALCULATE_MONITOR_REASON_WORKSPACE_CHANGE: return RECALCULATE_REASON_WORKSPACE_CHANGE;
case CLayoutManager::RECALCULATE_MONITOR_REASON_TOGGLE_FULLSCREEN: return RECALCULATE_REASON_TOGGLE_FULLSCREEN;
default: return RECALCULATE_REASON_UNKNOWN;
}
}

View file

@ -12,6 +12,18 @@
#include <expected>
namespace Layout {
enum eRecalculateReason : uint8_t {
RECALCULATE_REASON_UNKNOWN, // when the recalculate reason is unknown or not important to preserve
RECALCULATE_REASON_WORKSPACE_CHANGE,
RECALCULATE_REASON_SPECIAL_WORKSPACE_TOGGLE,
RECALCULATE_REASON_TOGGLE_FULLSCREEN,
RECALCULATE_REASON_INVALIDATE_MONITOR_GEOMETRIES,
RECALCULATE_REASON_RENDER_MOINTOR,
};
eRecalculateReason recalcMonitorReasonToRecalcReason(CLayoutManager::eRecalculateMonitorReason reason);
class ITarget;
class CAlgorithm;
@ -34,7 +46,7 @@ namespace Layout {
void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool silent);
void recalculate();
void recalculate(eRecalculateReason reason = RECALCULATE_REASON_UNKNOWN);
void toggleTargetFloating(SP<ITarget> t);
@ -68,4 +80,6 @@ namespace Layout {
// for recalc
CHyprSignalListener m_geomUpdateCallback;
};
bool isHardRecalculateReason(eRecalculateReason reason);
};

View file

@ -2002,7 +2002,7 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) {
if (pMonitor->m_scheduledRecalc) {
pMonitor->m_scheduledRecalc = false;
if (pMonitor->m_activeWorkspace) // might be missing (mirror)
pMonitor->m_activeWorkspace->m_space->recalculate();
pMonitor->m_activeWorkspace->m_space->recalculate(Layout::RECALCULATE_REASON_RENDER_MOINTOR);
}
if (!pMonitor->m_output->needsFrame && pMonitor->m_forceFullFrames == 0)