From 202cf48ecf627839c0a7433cbeb018c744214390 Mon Sep 17 00:00:00 2001 From: UjinT34 <41110182+UjinT34@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:31:48 +0300 Subject: [PATCH 01/48] cmakelists: search for any possible lua package name (#14204) --- CMakeLists.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 06cda39c0..d32bc5455 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -270,7 +270,9 @@ pkg_check_modules( re2 muparser lcms2 - lua55) +) + +pkg_search_module(LUA REQUIRED IMPORTED_TARGET GLOBAL lua55 lua5.5 lua-55 lua-5.5 lua>=5.5 lua<5.6) find_package(hyprwayland-scanner 0.3.10 REQUIRED) @@ -288,8 +290,8 @@ add_library(hyprland_lib STATIC ${SRCFILES}) add_executable(Hyprland src/main.cpp ${TRACY_CPP_FILES}) target_link_libraries(Hyprland hyprland_lib) -target_include_directories(hyprland_lib PUBLIC ${deps_INCLUDE_DIRS}) -target_include_directories(Hyprland PUBLIC ${deps_INCLUDE_DIRS}) +target_include_directories(hyprland_lib PUBLIC ${deps_INCLUDE_DIRS} ${LUA_INCLUDE_DIRS}) +target_include_directories(Hyprland PUBLIC ${deps_INCLUDE_DIRS} ${LUA_INCLUDE_DIRS}) set(USE_GPROF OFF) @@ -423,6 +425,7 @@ target_link_libraries( PkgConfig::hyprcursor_dep PkgConfig::hyprgraphics_dep PkgConfig::deps + PkgConfig::LUA ) target_link_libraries( From 45ffaee09361110c018e962dbb3d93f7f1ee8e09 Mon Sep 17 00:00:00 2001 From: Nikolai Nechaev Date: Thu, 30 Apr 2026 02:32:06 +0900 Subject: [PATCH 02/48] tests: Fix more tests failing on CI (#14159) * tests: Relax color management requirements in the colors test This continues the work from d4dd299 (#14142): - Fixes a missed check assuming a fixed value for `colorManagementPreset` - Stricten checks to require that the expected keywords are present as json keys * tests: Refactor `Tests::getAttribute` (was `Tests::getWindowAttribute`) - Improve ergonomics by consistently handling attribute name; - Remove 'window' from the function name, since it is compatible with other responses too; - Document the function. * tests: Fix solitary test assuming it knows the complete set of blockers The test was failing on the CI because new blockers appeared, which were not expected by the text. Fix the test, so that it just checks the set of expected blockers is a subset of actual blockers --- hyprtester/src/tests/main/colors.cpp | 10 +++++----- hyprtester/src/tests/main/dwindle.cpp | 2 +- .../src/tests/main/follow_mouse_shrink.cpp | 6 +++--- hyprtester/src/tests/main/solitary.cpp | 19 +++++++++++++++---- hyprtester/src/tests/main/window.cpp | 14 +++++++------- hyprtester/src/tests/shared.cpp | 11 +++++++---- hyprtester/src/tests/shared.hpp | 8 +++++++- 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/hyprtester/src/tests/main/colors.cpp b/hyprtester/src/tests/main/colors.cpp index 93dcbb651..e430145c5 100644 --- a/hyprtester/src/tests/main/colors.cpp +++ b/hyprtester/src/tests/main/colors.cpp @@ -8,7 +8,7 @@ TEST_CASE(monitorsColorManagement) { std::string monitorsSpec = getFromSocket("j/monitors"); - ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset")"); + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); ASSERT_CONTAINS(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', bitdepth = 10, cm = 'wide' })"), "ok"); @@ -16,14 +16,14 @@ TEST_CASE(monitorsColorManagement) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); monitorsSpec = getFromSocket("j/monitors"); - ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": "wide")"); + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); ASSERT_CONTAINS(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', bitdepth = 10, cm = 'srgb', sdrbrightness = 1.2, sdrsaturation = 0.98 })"), "ok"); monitorsSpec = getFromSocket("j/monitors"); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - ASSERT_CONTAINS(monitorsSpec, "colorManagementPreset"); - ASSERT_CONTAINS(monitorsSpec, "sdrBrightness"); - ASSERT_CONTAINS(monitorsSpec, "sdrSaturation"); + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); + ASSERT_CONTAINS(monitorsSpec, R"("sdrBrightness": )"); + ASSERT_CONTAINS(monitorsSpec, R"("sdrSaturation": )"); } diff --git a/hyprtester/src/tests/main/dwindle.cpp b/hyprtester/src/tests/main/dwindle.cpp index 5641f8acf..e962efd1b 100644 --- a/hyprtester/src/tests/main/dwindle.cpp +++ b/hyprtester/src/tests/main/dwindle.cpp @@ -204,7 +204,7 @@ TEST_CASE(dwindleForceSplitOnMoveToWorkspace) { OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); ASSERT(!!Tests::spawnKitty("kitty"), true); - std::string posBefore = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + std::string posBefore = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); OK(getFromSocket("/dispatch hl.dsp.cursor.move_to_corner({ corner = 3 })")); // top left diff --git a/hyprtester/src/tests/main/follow_mouse_shrink.cpp b/hyprtester/src/tests/main/follow_mouse_shrink.cpp index 0e12915b0..600643c6b 100644 --- a/hyprtester/src/tests/main/follow_mouse_shrink.cpp +++ b/hyprtester/src/tests/main/follow_mouse_shrink.cpp @@ -9,9 +9,9 @@ static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) { std::string activeWin = getFromSocket("/activewindow"); - auto winClass = Tests::getWindowAttribute(activeWin, "class:"); - auto winFullscreen = Tests::getWindowAttribute(activeWin, "fullscreen:").back(); - if (winClass.substr(strlen("class: ")) == class_ && winFullscreen == fullscreen) + auto winClass = Tests::getAttribute(activeWin, "class"); + auto winFullscreen = Tests::getAttribute(activeWin, "fullscreen").back(); + if (winClass == class_ && winFullscreen == fullscreen) return true; else { if (log) diff --git a/hyprtester/src/tests/main/solitary.cpp b/hyprtester/src/tests/main/solitary.cpp index 3e124a1e9..522b7968f 100644 --- a/hyprtester/src/tests/main/solitary.cpp +++ b/hyprtester/src/tests/main/solitary.cpp @@ -1,8 +1,11 @@ #include "tests.hpp" #include "../../shared.hpp" #include "../../hyprctlCompat.hpp" -#include +#include #include +#include +#include +#include #include #include #include "../shared.hpp" @@ -13,6 +16,13 @@ using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer +SUBTEST(expectBlockedByAll, const std::string& blockedByLine, const std::set& expectedBlockedBy) { + const std::set blockedBy = blockedByLine | std::ranges::views::split(',') | std::ranges::to>(); + NLog::log("blockedBy = {}", blockedBy); + NLog::log("expectedBlockedBy = {}", expectedBlockedBy); + ASSERT(std::ranges::includes(blockedBy, expectedBlockedBy), true); +} + TEST_CASE(solitaryClients) { OK(getFromSocket("/eval hl.config({ general = { allow_tearing = false } })")); OK(getFromSocket("/eval hl.config({ render = { direct_scanout = 0 } })")); @@ -21,11 +31,12 @@ TEST_CASE(solitaryClients) { { auto str = getFromSocket("/monitors"); EXPECT_CONTAINS(str, "solitary: 0\n"); - EXPECT_CONTAINS(str, "solitaryBlockedBy: windowed mode,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "solitaryBlockedBy"), {"windowed mode", "missing candidate"}); EXPECT_CONTAINS(str, "activelyTearing: false"); - EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,user settings,not supported by monitor,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "tearingBlockedBy"), + {"next frame is not torn", "user settings", "not supported by monitor", "missing candidate"}); EXPECT_CONTAINS(str, "directScanoutTo: 0\n"); - EXPECT_CONTAINS(str, "directScanoutBlockedBy: user settings,software renders/cursors,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "directScanoutBlockedBy"), {"user settings", "software renders/cursors", "missing candidate"}); } // FIXME: need a reliable client with solitary opaque surface in fullscreen. kitty doesn't work all the time diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp index 7f36cf7b7..4a4da0ccb 100644 --- a/hyprtester/src/tests/main/window.cpp +++ b/hyprtester/src/tests/main/window.cpp @@ -75,7 +75,7 @@ TEST_CASE(swapWindow) { // Test swapwindow by direction { getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos); OK(getFromSocket("/dispatch hl.dsp.window.swap({ direction = 'right' })")); @@ -87,7 +87,7 @@ TEST_CASE(swapWindow) { // Test swapwindow by class { getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with class:kitty_B", Colors::YELLOW, pos); OK(getFromSocket("/dispatch hl.dsp.window.swap({ target = 'class:kitty_B' })")); @@ -101,7 +101,7 @@ TEST_CASE(swapWindow) { getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"); auto addr = getWindowAddress(getFromSocket("/activewindow")); getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with address:0x{}(kitty_B)", Colors::YELLOW, pos, addr); OK(getFromSocket(std::format("/dispatch hl.dsp.window.swap({{ target = 'address:0x{}' }})", addr))); @@ -124,7 +124,7 @@ TEST_CASE(swapWindow) { { getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"); auto addr = getWindowAddress(getFromSocket("/activewindow")); - auto ws = Tests::getWindowAttribute(getFromSocket("/activewindow"), "workspace:"); + auto ws = "workspace: " + Tests::getAttribute(getFromSocket("/activewindow"), "workspace"); NLog::log("{}Sending address:0x{}(kitty_B) to workspace \"swapwindow2\"", Colors::YELLOW, addr); OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'name:swapwindow2', follow = false })")); @@ -190,9 +190,9 @@ TEST_CASE(windowGroupRules) { static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) { std::string activeWin = getFromSocket("/activewindow"); - auto winClass = Tests::getWindowAttribute(activeWin, "class:"); - auto winFullscreen = Tests::getWindowAttribute(activeWin, "fullscreen:").back(); - if (winClass.substr(strlen("class: ")) == class_ && winFullscreen == fullscreen) + auto winClass = Tests::getAttribute(activeWin, "class"); + auto winFullscreen = Tests::getAttribute(activeWin, "fullscreen").back(); + if (winClass == class_ && winFullscreen == fullscreen) return true; else { if (log) diff --git a/hyprtester/src/tests/shared.cpp b/hyprtester/src/tests/shared.cpp index 03a647519..d08f4ea23 100644 --- a/hyprtester/src/tests/shared.cpp +++ b/hyprtester/src/tests/shared.cpp @@ -1,4 +1,5 @@ #include "shared.hpp" +#include #include #include #include @@ -186,12 +187,14 @@ bool Tests::writeFile(const std::string& name, const std::string& contents) { return true; } -std::string Tests::getWindowAttribute(const std::string& winInfo, const std::string& attr) { - auto pos = winInfo.find(attr); +std::string Tests::getAttribute(const std::string& hyprlandResponse, std::string attr) { + attr += ": "; + auto pos = hyprlandResponse.find(attr); if (pos == std::string::npos) { NLog::log("{}Window attribute not found: '{}'", Colors::RED, attr); return "Wrong window attribute"; } - auto pos2 = winInfo.find('\n', pos); - return winInfo.substr(pos, pos2 - pos); + pos += attr.size(); + auto pos2 = hyprlandResponse.find('\n', pos); + return hyprlandResponse.substr(pos, pos2 - pos); } diff --git a/hyprtester/src/tests/shared.hpp b/hyprtester/src/tests/shared.hpp index 95058e040..fbef8a0e1 100644 --- a/hyprtester/src/tests/shared.hpp +++ b/hyprtester/src/tests/shared.hpp @@ -19,5 +19,11 @@ namespace Tests { bool killAllLayers(); std::string execAndGet(const std::string& cmd); bool writeFile(const std::string& name, const std::string& contents); - std::string getWindowAttribute(const std::string& winInfo, const std::string& attr); + /** + * Extracts the given attribute from Hyprland's response to requests such as `/clients`, `/workspaces`, etc. + * Automatically appends `: ` to `attr`. + * + * For example, `Tests::getAttribute(getFromSocket("/activewindow"), "at")` returns the active window's coordinates, e.g., `"2,32"` + */ + std::string getAttribute(const std::string& hyprlandResponse, std::string attr); }; From 9ee5ff1f7181cc5fa575f1702802ccb78c21ba12 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:31:06 +0100 Subject: [PATCH 03/48] animations: add springs (#14171) --- CMakeLists.txt | 2 +- example/hyprland.lua | 7 +- flake.lock | 6 +- .../lua/bindings/LuaBindingsConfigRules.cpp | 164 +++++++++++++----- .../lua/bindings/LuaBindingsInternal.cpp | 11 ++ .../lua/bindings/LuaBindingsInternal.hpp | 1 + src/managers/animation/AnimationManager.cpp | 11 +- 7 files changed, 142 insertions(+), 60 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d32bc5455..c1ffc6b3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,7 +131,7 @@ find_package(glslang CONFIG REQUIRED) set(AQUAMARINE_MINIMUM_VERSION 0.9.3) set(HYPRLANG_MINIMUM_VERSION 0.6.7) set(HYPRCURSOR_MINIMUM_VERSION 0.1.7) -set(HYPRUTILS_MINIMUM_VERSION 0.11.1) +set(HYPRUTILS_MINIMUM_VERSION 0.13.0) set(HYPRGRAPHICS_MINIMUM_VERSION 0.5.1) pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION}) diff --git a/example/hyprland.lua b/example/hyprland.lua index 16306bb22..dcc153428 100644 --- a/example/hyprland.lua +++ b/example/hyprland.lua @@ -139,10 +139,13 @@ hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1} } }) hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } }) +-- Default springs +hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 }) + hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) -hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" }) -hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) diff --git a/flake.lock b/flake.lock index 45c80cd38..8ebb11f87 100644 --- a/flake.lock +++ b/flake.lock @@ -261,11 +261,11 @@ ] }, "locked": { - "lastModified": 1777148223, - "narHash": "sha256-PTf7kRFFzCW6rIYxLH2fWfVJmj86FSYe3k6L8B+IM9o=", + "lastModified": 1777492286, + "narHash": "sha256-PwuoEJQcjSKJNP5T55qhfDwIP0tw5zxEhfu8GDfKfeg=", "owner": "hyprwm", "repo": "hyprutils", - "rev": "fa3992be2dfebe4ab06d753c6ca59bea298e798f", + "rev": "ec5c0c709706bad5b82f667fd8758eae442577ce", "type": "github" }, "original": { diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp index 52ecaf698..14e172649 100644 --- a/src/config/lua/bindings/LuaBindingsConfigRules.cpp +++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp @@ -40,9 +40,12 @@ #include "../../../managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp" #include "../../../managers/permissions/DynamicPermissionManager.hpp" +#include + using namespace Config; using namespace Config::Lua; using namespace Config::Lua::Bindings; +using namespace Hyprutils::Utils; namespace { struct SFieldDesc { @@ -286,53 +289,101 @@ static int hlCurve(lua_State* L) { const auto& curveType = typeParser.parsed(); - if (curveType != "bezier") - return Internal::configError(L, std::format("hl.curve(\"{}\"): unknown curve type \"{}\", expected \"bezier\"", name, curveType)); - - lua_getfield(L, 2, "points"); - if (!lua_istable(L, -1)) { - lua_pop(L, 1); - return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name)); - } - int pointsIdx = lua_gettop(L); - - if (luaL_len(L, pointsIdx) != 2) { - lua_pop(L, 1); - return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name)); - } - - float coords[4] = {}; - for (int pt = 1; pt <= 2; pt++) { - lua_rawgeti(L, pointsIdx, pt); - if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) { - lua_pop(L, 2); - return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt)); - } - int ptIdx = lua_gettop(L); - - for (int comp = 0; comp < 2; comp++) { - lua_rawgeti(L, ptIdx, comp + 1); - CLuaConfigFloat coordParser(0.F, -1.F, 2.F); - auto coordErr = coordParser.parse(L); + if (curveType == "bezier") { + lua_getfield(L, 2, "points"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); - if (coordErr.errorCode != PARSE_ERROR_OK) { - lua_pop(L, 2); - return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message)); - } - coords[((pt - 1) * 2) + comp] = coordParser.parsed(); + return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name)); + } + int pointsIdx = lua_gettop(L); + + if (luaL_len(L, pointsIdx) != 2) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name)); } - lua_pop(L, 1); - } - lua_pop(L, 1); + float coords[4] = {}; + for (int pt = 1; pt <= 2; pt++) { + lua_rawgeti(L, pointsIdx, pt); + if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) { + lua_pop(L, 2); + return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt)); + } + int ptIdx = lua_gettop(L); + + for (int comp = 0; comp < 2; comp++) { + lua_rawgeti(L, ptIdx, comp + 1); + CLuaConfigFloat coordParser(0.F, -1.F, 2.F); + auto coordErr = coordParser.parse(L); + lua_pop(L, 1); + if (coordErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 2); + return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message)); + } + coords[((pt - 1) * 2) + comp] = coordParser.parsed(); + } + + lua_pop(L, 1); + } + lua_pop(L, 1); + + g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3])); + } else if (curveType == "spring") { + + Hyprutils::Animation::SSpringCurve curve; + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "stiffness"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number", name)); + + curve.stiffness = lua_tonumber(L, -1); + + if (curve.stiffness <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number >= 0.5", name)); + } + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "dampening"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number", name)); + + curve.damping = lua_tonumber(L, -1); + + if (curve.damping <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number >= 0.5", name)); + } + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "mass"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number", name)); + + curve.mass = lua_tonumber(L, -1); + + if (curve.mass <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number >= 0.5", name)); + } + + g_pAnimationManager->addSpringWithName(name, curve); + } else + return Internal::configError(L, std::format(R"(hl.curve("{}"): unknown curve type "{}", expected "bezier" or "spring")", name, curveType)); - g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3])); return 0; } static int hlAnimation(lua_State* L) { if (!lua_istable(L, 1)) - return Internal::configError(L, "hl.animation: expected a table, e.g. { leaf = \"global\", enabled = true, speed = 5, bezier = \"default\" }"); + return Internal::configError(L, R"(hl.animation: expected a table, e.g. { leaf = "global", enabled = true, speed = 5, bezier = "default" })"); CLuaConfigString leafParser(""); auto leafErr = Internal::parseTableField(L, 1, "leaf", leafParser); @@ -366,15 +417,34 @@ static int hlAnimation(lua_State* L) { if (speed <= 0) return Internal::configError(L, std::format("hl.animation(\"{}\"): speed must be greater than 0", leaf)); - CLuaConfigString bezierParser(""); - auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser); - if (bezierErr.errorCode != PARSE_ERROR_OK) - return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message)); + std::string curveName; - const auto& bezierName = bezierParser.parsed(); + if (Internal::hasTableField(L, 1, "bezier")) { + CLuaConfigString bezierParser(""); + auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser); + if (bezierErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message)); - if (!g_pAnimationManager->bezierExists(bezierName)) - return Internal::configError(L, std::format("hl.animation(\"{}\"): no such bezier \"{}\"", leaf, bezierName)); + const auto& bezierName = bezierParser.parsed(); + + if (!g_pAnimationManager->bezierExists(bezierName)) + return Internal::configError(L, std::format(R"(hl.animation("{}"): no such bezier "{}")", leaf, bezierName)); + + curveName = bezierName; + } else if (Internal::hasTableField(L, 1, "spring")) { + CLuaConfigString springParser(""); + auto springErr = Internal::parseTableField(L, 1, "spring", springParser); + if (springErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, springErr.message)); + + const auto& springName = springParser.parsed(); + + if (!g_pAnimationManager->springExists(springName)) + return Internal::configError(L, std::format(R"(hl.animation("{}"): no such spring "{}")", leaf, springName)); + + curveName = "spring:" + springName; + } else + return Internal::configError(L, std::format(R"(hl.animation("{}"): bezier or spring is required)", leaf)); std::string style; lua_getfield(L, 1, "style"); @@ -383,7 +453,7 @@ static int hlAnimation(lua_State* L) { auto styleErr = styleParser.parse(L); if (styleErr.errorCode != PARSE_ERROR_OK) { lua_pop(L, 1); - return Internal::configError(L, std::format("hl.animation(\"{}\"): field \"style\": {}", leaf, styleErr.message)); + return Internal::configError(L, std::format(R"(hl.animation("{}"): field "style": {})", leaf, styleErr.message)); } style = styleParser.parsed(); } @@ -395,7 +465,7 @@ static int hlAnimation(lua_State* L) { return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, err)); } - Config::animationTree()->setConfigForNode(leaf, true, speed, bezierName, style); + Config::animationTree()->setConfigForNode(leaf, true, speed, curveName, style); return 0; } diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp index a6af42f89..e0cef1464 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.cpp +++ b/src/config/lua/bindings/LuaBindingsInternal.cpp @@ -574,3 +574,14 @@ std::expected, int> Internal::buildRuleFromTable( return rule; } + +bool Internal::hasTableField(lua_State* L, int tableIdx, const char* field) { + lua_getfield(L, tableIdx, field); + if (lua_isnoneornil(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pop(L, 1); + return true; +} diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 5d4a8d87f..0cced6b47 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -196,6 +196,7 @@ namespace Config::Lua::Bindings::Internal { return err; } + bool hasTableField(lua_State* L, int tableIdx, const char* field); void registerToplevelBindings(lua_State* L, CConfigManager* mgr); void registerQueryBindings(lua_State* L); void registerNotificationBindings(lua_State* L); diff --git a/src/managers/animation/AnimationManager.cpp b/src/managers/animation/AnimationManager.cpp index 687f5f2c8..c5c3ca542 100644 --- a/src/managers/animation/AnimationManager.cpp +++ b/src/managers/animation/AnimationManager.cpp @@ -42,7 +42,7 @@ static void updateVariable(CAnimatedVariable& av, const float POINTY, b av.value() = av.begun() + DELTA * POINTY; } -static void updateColorVariable(CAnimatedVariable& av, const float POINTY, bool warp) { +static void updateColorVariable(CAnimatedVariable& av, const float POINTY, bool warp = false) { if (warp || av.value() == av.goal()) { av.warp(true, false); return; @@ -139,15 +139,12 @@ static void handleUpdate(CAnimatedVariable& av, bool warp) { animationsDisabled = animationsDisabled || ls->m_ruleApplicator->noanim().valueOrDefault(); } - const auto SPENT = av.getPercent(); - const auto PBEZIER = g_pAnimationManager->getBezier(av.getBezierName()); - const auto POINTY = PBEZIER->getYForPoint(SPENT); - const bool WARP = animationsDisabled || SPENT >= 1.f; + const auto STEP = av.getCurveStep(); if constexpr (std::same_as) - updateColorVariable(av, POINTY, WARP); + updateColorVariable(av, STEP.value, STEP.finished); else - updateVariable(av, POINTY, WARP); + updateVariable(av, STEP.value, STEP.finished); av.onUpdate(); } From 6bd15b948f1b266820c286c8f41fa7b4c2a12399 Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Thu, 30 Apr 2026 00:04:28 +0300 Subject: [PATCH 04/48] main: improve error reporting during initialization in main.cpp (#14181) --- src/main.cpp | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 566a2544b..faf780832 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -100,14 +100,15 @@ int main(int argc, char** argv) { return 1; } + const auto FD_STR = *std::next(it); try { - socketFd = std::stoi(*std::next(it)); + socketFd = std::stoi(FD_STR); // check if socketFd is a valid file descriptor if (fcntl(socketFd, F_GETFD) == -1) - throw std::exception(); - } catch (...) { - std::println(stderr, "[ ERROR ] Invalid Wayland FD!"); + throw std::runtime_error("invalid or closed file descriptor"); + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Invalid Wayland FD '{}': {}!", __LINE__, FD_STR, e.what()); help(); return 1; @@ -123,13 +124,14 @@ int main(int argc, char** argv) { configPath = *std::next(it); try { - configPath = std::filesystem::canonical(configPath); + const auto ABS_PATH = std::filesystem::canonical(configPath); - if (!std::filesystem::is_regular_file(configPath)) { - throw std::exception(); + if (!std::filesystem::is_regular_file(ABS_PATH)) { + throw std::runtime_error("not a regular file"); } - } catch (...) { - std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath); + configPath = ABS_PATH; + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Config file '{}' is invalid: {}!", __LINE__, configPath, e.what()); help(); return 1; @@ -165,11 +167,12 @@ int main(int argc, char** argv) { return 1; } + const auto WATCHDOG_STR = *std::next(it); try { - watchdogFd = std::stoi(*std::next(it)); + watchdogFd = std::stoi(WATCHDOG_STR); it++; - } catch (...) { - std::println(stderr, "[ ERROR ] Invalid fd for watchdog fd"); + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Invalid watchdog FD '{}': {}!", __LINE__, WATCHDOG_STR, e.what()); help(); return 1; } From a92dae5af7bad3af53f6d6b689d71ee900077569 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Wed, 29 Apr 2026 16:37:48 +0100 Subject: [PATCH 05/48] desktop/window: add alpha container for alpha calculations --- hyprtester/plugin/src/main.cpp | 2 +- src/Compositor.cpp | 7 +- src/desktop/types/MultiAnimatedVariable.hpp | 184 ++++++++++++++++++ src/desktop/view/Window.cpp | 88 ++++++--- src/desktop/view/Window.hpp | 39 ++-- .../animation/DesktopAnimationManager.cpp | 23 +-- .../input/trackpad/gestures/CloseGesture.cpp | 24 +-- src/managers/screenshare/ScreenshareFrame.cpp | 3 +- src/render/OpenGL.cpp | 4 +- src/render/Renderer.cpp | 27 +-- 10 files changed, 324 insertions(+), 77 deletions(-) create mode 100644 src/desktop/types/MultiAnimatedVariable.hpp diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index ac59119de..c7d6f4fdf 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -336,7 +336,7 @@ static SDispatchResult floatingFocusOnFullscreen(std::string in) { if (!PLASTWINDOW->m_isFloating) return {.success = false, .error = "Window must be floating"}; - if (PLASTWINDOW->m_alpha != 1.f) + if (PLASTWINDOW->alphaTotal() != 1.F) return {.success = false, .error = "floating window doesnt restore it opacity when focused on fullscreen workspace"}; if (!PLASTWINDOW->m_createdOverFullscreen) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index f0261847d..3b7ea8154 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -99,6 +99,7 @@ using namespace Hyprutils::String; using namespace Aquamarine; using enum NContentType::eContentType; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render::GL; static int handleCritSignal(int signo, void* data) { @@ -1368,7 +1369,7 @@ void CCompositor::cleanupFadingOut(const MONITORID& monid) { if (w->monitorID() != monid && w->m_monitor) continue; - if (!w->m_fadingOut || w->m_alpha->value() == 0.f) { + if (!w->m_fadingOut || w->alphaValue(WINDOW_ALPHA_FADE) == 0.f) { w->m_fadingOut = false; @@ -2700,8 +2701,8 @@ void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWor g_pCompositor->updateSuspendedStates(); if (!WASVISIBLE && pWindow->m_workspace && pWindow->m_workspace->isVisible()) { - pWindow->m_movingFromWorkspaceAlpha->setValueAndWarp(0.F); - *pWindow->m_movingFromWorkspaceAlpha = 1.F; + pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(0.F); + *pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE) = 1.F; } } diff --git a/src/desktop/types/MultiAnimatedVariable.hpp b/src/desktop/types/MultiAnimatedVariable.hpp new file mode 100644 index 000000000..564a2db36 --- /dev/null +++ b/src/desktop/types/MultiAnimatedVariable.hpp @@ -0,0 +1,184 @@ +#pragma once + +#include +#include + +#include "../../helpers/AnimatedVariable.hpp" + +namespace Desktop::Types { + + template + struct SAddOperation { + static constexpr T identity() { + return T{}; + } + + static T apply(const T& lhs, const T& rhs) { + return lhs + rhs; + } + }; + + template + struct SMultiplyOperation { + static constexpr T identity() { + return T{1}; + } + + static T apply(const T& lhs, const T& rhs) { + return lhs * rhs; + } + }; + + template > + class CMultiAnimatedVariableContainer { + public: + using value_type = VarType; + using key_type = Key; + using operation_type = Operation; + + static constexpr size_t size() { + return Count; + } + + PHLANIMVAR& get(const Key key) { + return m_vars.at(index(key)); + } + + const PHLANIMVAR& get(const Key key) const { + return m_vars.at(index(key)); + } + + PHLANIMVAR& operator[](const Key key) { + return get(key); + } + + const PHLANIMVAR& operator[](const Key key) const { + return get(key); + } + + PHLANIMVAR& raw(const size_t i) { + return m_vars.at(i); + } + + const PHLANIMVAR& raw(const size_t i) const { + return m_vars.at(i); + } + + std::array, Count>& all() { + return m_vars; + } + + const std::array, Count>& all() const { + return m_vars; + } + + template + void forEach(Fn&& fn) { + for (size_t i = 0; i < Count; ++i) + fn(static_cast(i), m_vars.at(i)); + } + + template + void forEach(Fn&& fn) const { + for (size_t i = 0; i < Count; ++i) + fn(static_cast(i), m_vars.at(i)); + } + + VarType getTotal() const { + return accumulate([](const auto& var) { return var->value(); }); + } + + VarType getTotalGoal() const { + return accumulate([](const auto& var) { return var->goal(); }); + } + + VarType getTotalBegun() const { + return accumulate([](const auto& var) { return var->begun(); }); + } + + VarType getTotalWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->value(); }); + } + + VarType getTotalGoalWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->goal(); }); + } + + VarType getTotalBegunWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->begun(); }); + } + + VarType value() const { + return getTotal(); + } + + VarType goal() const { + return getTotalGoal(); + } + + bool isBeingAnimated() const { + for (const auto& var : m_vars) { + if (var && var->isBeingAnimated()) + return true; + } + + return false; + } + + bool initialized() const { + for (const auto& var : m_vars) { + if (!var) + return false; + } + + return true; + } + + void warp(const bool endCallback = true) { + for (auto& var : m_vars) { + if (var) + var->warp(endCallback); + } + } + + private: + static constexpr size_t index(const Key key) { + return static_cast(key); + } + + template + VarType accumulate(Getter&& getter) const { + VarType result = Operation::identity(); + + for (const auto& var : m_vars) { + if (!var) + continue; + + result = Operation::apply(result, getter(var)); + } + + return result; + } + + template + VarType accumulateExcept(const Key key, Getter&& getter) const { + VarType result = Operation::identity(); + const size_t except = index(key); + + for (size_t i = 0; i < Count; ++i) { + if (i == except || !m_vars.at(i)) + continue; + + result = Operation::apply(result, getter(m_vars.at(i))); + } + + return result; + } + + std::array, Count> m_vars; + }; + + template > + using CMultiAVarContainer = CMultiAnimatedVariableContainer; + +} diff --git a/src/desktop/view/Window.cpp b/src/desktop/view/Window.cpp index cbcdb5991..ef854bd13 100644 --- a/src/desktop/view/Window.cpp +++ b/src/desktop/view/Window.cpp @@ -84,13 +84,17 @@ PHLWINDOW CWindow::create(SP surface) { g_pAnimationManager->createAnimation(0.f, pWindow->m_borderFadeAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_BORDER); g_pAnimationManager->createAnimation(0.f, pWindow->m_borderAngleAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("borderangle"), pWindow, AVARDAMAGE_BORDER); - g_pAnimationManager->createAnimation(1.f, pWindow->m_alpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->m_activeInactiveAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingToWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingFromWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, + AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, + AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, Config::animationTree()->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE); pWindow->addWindowDeco(makeUnique(pWindow)); @@ -114,13 +118,17 @@ PHLWINDOW CWindow::create(SP resource) { g_pAnimationManager->createAnimation(0.f, pWindow->m_borderFadeAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_BORDER); g_pAnimationManager->createAnimation(0.f, pWindow->m_borderAngleAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("borderangle"), pWindow, AVARDAMAGE_BORDER); - g_pAnimationManager->createAnimation(1.f, pWindow->m_alpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->m_activeInactiveAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingToWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingFromWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, + AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, + AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, Config::animationTree()->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE); pWindow->addWindowDeco(makeUnique(pWindow)); @@ -179,7 +187,7 @@ eViewType CWindow::type() const { } bool CWindow::visible() const { - return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || (m_fadingOut && m_alpha->value() != 0.F)); + return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || (m_fadingOut && alphaValue(WINDOW_ALPHA_FADE) != 0.F)); } std::optional CWindow::logicalBox() const { @@ -494,9 +502,12 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) { const auto OLDWORKSPACE = m_workspace; if (OLDWORKSPACE->isVisible()) { - m_movingToWorkspaceAlpha->setValueAndWarp(1.F); - *m_movingToWorkspaceAlpha = 0.F; - m_movingToWorkspaceAlpha->setCallbackOnEnd([this](auto) { m_monitorMovedFrom = -1; }); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); + *alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE) = 0.F; + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setCallbackOnEnd([this](auto) { + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); + m_monitorMovedFrom = -1; + }); m_monitorMovedFrom = OLDWORKSPACE ? OLDWORKSPACE->monitorID() : -1; } @@ -615,15 +626,18 @@ void CWindow::onMap() { m_realSize->resetAllCallbacks(); m_borderFadeAnimationProgress->resetAllCallbacks(); m_borderAngleAnimationProgress->resetAllCallbacks(); - m_activeInactiveAlpha->resetAllCallbacks(); - m_alpha->resetAllCallbacks(); + alpha(WINDOW_ALPHA_ACTIVE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_FADE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_FULLSCREEN)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_LAYOUT)->resetAllCallbacks(); m_realShadowColor->resetAllCallbacks(); m_realGlowColor->resetAllCallbacks(); m_dimPercent->resetAllCallbacks(); - m_movingToWorkspaceAlpha->resetAllCallbacks(); - m_movingFromWorkspaceAlpha->resetAllCallbacks(); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->resetAllCallbacks(); - m_movingFromWorkspaceAlpha->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); if (m_borderAngleAnimationProgress->enabled()) { m_borderAngleAnimationProgress->setValueAndWarp(0.f); @@ -666,7 +680,7 @@ void CWindow::onMap() { } }); - m_movingFromWorkspaceAlpha->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(1.F); m_reportedSize = m_pendingReportedSize; m_animatingIn = true; @@ -713,6 +727,30 @@ bool CWindow::isHidden() { return m_hidden; } +PHLANIMVAR& CWindow::alpha(eWindowAlpha type) { + return m_alpha.get(type); +} + +const PHLANIMVAR& CWindow::alpha(eWindowAlpha type) const { + return m_alpha.get(type); +} + +float CWindow::alphaValue(eWindowAlpha type) const { + return alpha(type)->value(); +} + +float CWindow::alphaGoal(eWindowAlpha type) const { + return alpha(type)->goal(); +} + +float CWindow::alphaTotal() const { + return m_alpha.getTotal(); +} + +float CWindow::alphaTotalWithout(eWindowAlpha type) const { + return m_alpha.getTotalWithout(type); +} + // check if the point is "hidden" under a rounded corner of the window // it is assumed that the point is within the real window box (m_vRealPosition, m_vRealSize) // otherwise behaviour is undefined @@ -762,7 +800,7 @@ Vector2D CWindow::middle() { } bool CWindow::opaque() { - if (m_alpha->value() != 1.f || m_activeInactiveAlpha->value() != 1.f) + if (alphaValue(WINDOW_ALPHA_FADE) != 1.f || alphaValue(WINDOW_ALPHA_FULLSCREEN) != 1.f || alphaValue(WINDOW_ALPHA_ACTIVE) != 1.f) return false; const auto PWORKSPACE = m_workspace; @@ -1593,12 +1631,12 @@ void CWindow::updateDecorationValues() { // opacity const auto PWORKSPACE = m_workspace; if (isEffectiveInternalFSMode(FSMODE_FULLSCREEN)) { - *m_activeInactiveAlpha = m_ruleApplicator->alphaFullscreen().valueOrDefault().applyAlpha(*PFULLSCREENALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alphaFullscreen().valueOrDefault().applyAlpha(*PFULLSCREENALPHA); } else { if (m_self == Desktop::focusState()->window()) - *m_activeInactiveAlpha = m_ruleApplicator->alpha().valueOrDefault().applyAlpha(*PACTIVEALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alpha().valueOrDefault().applyAlpha(*PACTIVEALPHA); else - *m_activeInactiveAlpha = m_ruleApplicator->alphaInactive().valueOrDefault().applyAlpha(*PINACTIVEALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alphaInactive().valueOrDefault().applyAlpha(*PINACTIVEALPHA); } // dim @@ -2056,10 +2094,10 @@ void CWindow::mapWindow() { } else Desktop::focusState()->fullWindowFocus(m_self.lock(), FOCUS_REASON_NEW_WINDOW); - m_activeInactiveAlpha->setValueAndWarp(*PACTIVEALPHA); + alpha(WINDOW_ALPHA_ACTIVE)->setValueAndWarp(*PACTIVEALPHA); m_dimPercent->setValueAndWarp(m_ruleApplicator->noDim().valueOrDefault() ? 0.f : *PDIMSTRENGTH); } else { - m_activeInactiveAlpha->setValueAndWarp(*PINACTIVEALPHA); + alpha(WINDOW_ALPHA_ACTIVE)->setValueAndWarp(*PINACTIVEALPHA); m_dimPercent->setValueAndWarp(0); } @@ -2129,7 +2167,7 @@ void CWindow::mapWindow() { updateDecorationValues(); // avoid this window being visible if (PWORKSPACE->m_hasFullscreenWindow && !isFullscreen() && !m_isFloating) - m_alpha->setValueAndWarp(0.f); + alpha(WINDOW_ALPHA_FULLSCREEN)->setValueAndWarp(0.f); g_pCompositor->setPreferredScaleForSurface(wlSurface()->resource(), PMONITOR->m_scale); g_pCompositor->setPreferredTransformForSurface(wlSurface()->resource(), PMONITOR->m_transform); diff --git a/src/desktop/view/Window.hpp b/src/desktop/view/Window.hpp index ff6fad96e..0a468ee33 100644 --- a/src/desktop/view/Window.hpp +++ b/src/desktop/view/Window.hpp @@ -13,6 +13,7 @@ #include "../../render/decorations/IHyprWindowDecoration.hpp" #include "../../render/Transformer.hpp" #include "../DesktopTypes.hpp" +#include "../types/MultiAnimatedVariable.hpp" #include "Popup.hpp" #include "Subsurface.hpp" #include "WLSurface.hpp" @@ -77,6 +78,17 @@ namespace Desktop::View { SUPPRESS_FULLSCREEN_OUTPUT = 1 << 4, }; + enum eWindowAlpha : uint8_t { + WINDOW_ALPHA_FADE = 0, + WINDOW_ALPHA_ACTIVE, + WINDOW_ALPHA_FULLSCREEN, + WINDOW_ALPHA_LAYOUT, + WINDOW_ALPHA_MOVE_TO_WORKSPACE, + WINDOW_ALPHA_MOVE_FROM_WORKSPACE, + + WINDOW_ALPHA_LAST, + }; + struct SWindowActiveEvent { PHLWINDOW window = nullptr; eFocusReason reason = sc(0) /* unknown */; @@ -194,13 +206,13 @@ namespace Desktop::View { mutable bool m_borderSizeCacheDirty = true; // Fade in-out - PHLANIMVAR m_alpha; - bool m_fadingOut = false; - bool m_readyToDelete = false; - Vector2D m_originalClosedPos; // these will be used for calculations later on in - Vector2D m_originalClosedSize; // drawing the closing animations - SBoxExtents m_originalClosedExtents; - bool m_animatingIn = false; + Desktop::Types::CMultiAVarContainer m_alpha; + bool m_fadingOut = false; + bool m_readyToDelete = false; + Vector2D m_originalClosedPos; // these will be used for calculations later on in + Vector2D m_originalClosedSize; // drawing the closing animations + SBoxExtents m_originalClosedExtents; + bool m_animatingIn = false; // For pinned (sticky) windows bool m_pinned = false; @@ -225,10 +237,6 @@ namespace Desktop::View { // Transformers std::vector> m_transformers; - // for alpha - PHLANIMVAR m_activeInactiveAlpha; - PHLANIMVAR m_movingFromWorkspaceAlpha; - // animated shadow color PHLANIMVAR m_realShadowColor; @@ -239,8 +247,7 @@ namespace Desktop::View { PHLANIMVAR m_dimPercent; // animate moving to an invisible workspace - int m_monitorMovedFrom = -1; // -1 means not moving - PHLANIMVAR m_movingToWorkspaceAlpha; + int m_monitorMovedFrom = -1; // -1 means not moving // swallowing PHLWINDOWREF m_swallowed; @@ -298,6 +305,12 @@ namespace Desktop::View { void onMap(); void setHidden(bool hidden); bool isHidden(); + PHLANIMVAR& alpha(eWindowAlpha type); + const PHLANIMVAR& alpha(eWindowAlpha type) const; + float alphaValue(eWindowAlpha type) const; + float alphaGoal(eWindowAlpha type) const; + float alphaTotal() const; + float alphaTotalWithout(eWindowAlpha type) const; void updateDecorationValues(); SBoxExtents getFullWindowReservedArea(); Vector2D middle(); diff --git a/src/managers/animation/DesktopAnimationManager.cpp b/src/managers/animation/DesktopAnimationManager.cpp index 186a3fa37..3ef9ff009 100644 --- a/src/managers/animation/DesktopAnimationManager.cpp +++ b/src/managers/animation/DesktopAnimationManager.cpp @@ -18,25 +18,26 @@ #include using namespace Hyprutils::String; +using namespace Desktop::View; void CDesktopAnimationManager::startAnimation(PHLWINDOW pWindow, eAnimationType type, bool force) { const bool CLOSE = type == ANIMATION_TYPE_OUT; if (CLOSE) - *pWindow->m_alpha = 0.F; + *pWindow->alpha(WINDOW_ALPHA_FADE) = 0.F; else { - pWindow->m_alpha->setValueAndWarp(0.F); - *pWindow->m_alpha = 1.F; + pWindow->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(0.F); + *pWindow->alpha(WINDOW_ALPHA_FADE) = 1.F; } if (!CLOSE) { pWindow->m_realPosition->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsIn")); pWindow->m_realSize->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsIn")); - pWindow->m_alpha->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeIn")); + pWindow->alpha(WINDOW_ALPHA_FADE)->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeIn")); } else { pWindow->m_realPosition->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsOut")); pWindow->m_realSize->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsOut")); - pWindow->m_alpha->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeOut")); + pWindow->alpha(WINDOW_ALPHA_FADE)->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeOut")); } std::string ANIMSTYLE = pWindow->m_realPosition->getStyle(); @@ -475,11 +476,11 @@ void CDesktopAnimationManager::setFullscreenFadeAnimation(PHLWORKSPACE ws, eAnim continue; if (!FULLSCREEN) - *w->m_alpha = 1.F; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = 1.F; else if (!w->isFullscreen()) { - const bool CREATED_OVER_FS = w->m_createdOverFullscreen; - const bool IS_IN_GROUP_OF_FS = FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(w); - *w->m_alpha = !CREATED_OVER_FS && !IS_IN_GROUP_OF_FS ? 0.f : 1.f; + const bool CREATED_OVER_FS = w->m_createdOverFullscreen; + const bool IS_IN_GROUP_OF_FS = FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(w); + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = !CREATED_OVER_FS && !IS_IN_GROUP_OF_FS ? 0.f : 1.f; } } } @@ -498,7 +499,7 @@ void CDesktopAnimationManager::setFullscreenFloatingFade(PHLWINDOW pWindow, floa if (pWindow->m_fadingOut || !pWindow->m_isFloating) return; - *pWindow->m_alpha = fade; + *pWindow->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; } void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, float fade, PHLWINDOW exclude) { @@ -510,7 +511,7 @@ void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, flo if (w->m_fadingOut || w->m_pinned || w->isFullscreen()) continue; - *w->m_alpha = fade; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; } } diff --git a/src/managers/input/trackpad/gestures/CloseGesture.cpp b/src/managers/input/trackpad/gestures/CloseGesture.cpp index 59ff28385..225f3132e 100644 --- a/src/managers/input/trackpad/gestures/CloseGesture.cpp +++ b/src/managers/input/trackpad/gestures/CloseGesture.cpp @@ -9,6 +9,8 @@ #include "../../../../desktop/state/FocusState.hpp" #include "../../../../layout/target/Target.hpp" +using namespace Desktop::View; + constexpr const float MAX_DISTANCE = 200.F; static std::vector> trackpadCloseTimers; @@ -33,18 +35,18 @@ void CCloseTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& if (!m_window) return; - m_alphaFrom = m_window->m_alpha->goal(); + m_alphaFrom = m_window->alphaGoal(WINDOW_ALPHA_FADE); m_posFrom = m_window->m_realPosition->goal(); m_sizeFrom = m_window->m_realSize->goal(); g_pDesktopAnimationManager->startAnimation(m_window.lock(), CDesktopAnimationManager::ANIMATION_TYPE_OUT, true); - *m_window->m_alpha = 0.f; + *m_window->alpha(WINDOW_ALPHA_FADE) = 0.f; - m_alphaTo = m_window->m_alpha->goal(); + m_alphaTo = m_window->alphaGoal(WINDOW_ALPHA_FADE); m_posTo = m_window->m_realPosition->goal(); m_sizeTo = m_window->m_realSize->goal(); - m_window->m_alpha->setValueAndWarp(m_alphaFrom); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(m_alphaFrom); m_window->m_realPosition->setValueAndWarp(m_posFrom); m_window->m_realSize->setValueAndWarp(m_sizeFrom); @@ -61,7 +63,7 @@ void CCloseTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdat const auto FADEPERCENT = std::clamp(m_lastDelta / MAX_DISTANCE, 0.F, 1.F); - m_window->m_alpha->setValueAndWarp(lerpVal(m_alphaFrom, m_alphaTo, FADEPERCENT)); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(lerpVal(m_alphaFrom, m_alphaTo, FADEPERCENT)); m_window->m_realPosition->setValueAndWarp(lerpVal(m_posFrom, m_posTo, FADEPERCENT)); m_window->m_realSize->setValueAndWarp(lerpVal(m_sizeFrom, m_sizeTo, FADEPERCENT)); @@ -81,20 +83,20 @@ void CCloseTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) if (COMPLETION < 0.2F) { // revert the animation g_pHyprRenderer->damageWindow(m_window.lock()); - *m_window->m_alpha = m_alphaFrom; - *m_window->m_realPosition = m_posFrom; - *m_window->m_realSize = m_sizeFrom; + *m_window->alpha(WINDOW_ALPHA_FADE) = m_alphaFrom; + *m_window->m_realPosition = m_posFrom; + *m_window->m_realSize = m_sizeFrom; return; } // commence. Close the window and restore our current state to avoid a harsh anim - const auto CURRENT_ALPHA = m_window->m_alpha->value(); + const auto CURRENT_ALPHA = m_window->alphaValue(WINDOW_ALPHA_FADE); const auto CURRENT_POS = m_window->m_realPosition->value(); const auto CURRENT_SIZE = m_window->m_realSize->value(); Desktop::focusState()->window()->sendClose(); - m_window->m_alpha->setValueAndWarp(CURRENT_ALPHA); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(CURRENT_ALPHA); m_window->m_realPosition->setValueAndWarp(CURRENT_POS); m_window->m_realSize->setValueAndWarp(CURRENT_SIZE); @@ -136,7 +138,7 @@ void CCloseTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) window->layoutTarget()->recalc(); window->updateDecorationValues(); window->sendWindowSize(true); - *window->m_alpha = 1.F; + *window->alpha(WINDOW_ALPHA_FADE) = 1.F; }, nullptr); trackpadCloseTimers.emplace_back(timer); diff --git a/src/managers/screenshare/ScreenshareFrame.cpp b/src/managers/screenshare/ScreenshareFrame.cpp index 66d649999..7a596d0c6 100644 --- a/src/managers/screenshare/ScreenshareFrame.cpp +++ b/src/managers/screenshare/ScreenshareFrame.cpp @@ -18,6 +18,7 @@ using namespace Hyprgraphics::Egl; using namespace Screenshare; +using namespace Desktop::View; CScreenshareFrame::CScreenshareFrame(WP session, bool overlayCursor, bool isFirst) : m_session(session), m_bufferSize(m_session->bufferSize()), m_overlayCursor(overlayCursor), m_isFirst(isFirst) { @@ -259,7 +260,7 @@ void CScreenshareFrame::renderMonitor() { const auto PWORKSPACE = w->m_workspace; - if UNLIKELY (!PWORKSPACE && !w->m_fadingOut && w->m_alpha->value() != 0.f) + if UNLIKELY (!PWORKSPACE && !w->m_fadingOut && w->alphaValue(WINDOW_ALPHA_FADE) * w->alphaValue(WINDOW_ALPHA_FULLSCREEN) != 0.f) continue; const auto renderOffset = PWORKSPACE && !w->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D{}; diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index a87ec2eef..08c50de10 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -59,6 +59,7 @@ using namespace Hyprutils::OS; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render; using namespace Render::GL; @@ -1867,7 +1868,8 @@ void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { const auto PSURFACE = pWindow->wlSurface()->resource(); const auto PWORKSPACE = pWindow->m_workspace; - const float A = pWindow->m_alpha->value() * pWindow->m_activeInactiveAlpha->value() * PWORKSPACE->m_alpha->value(); + const float A = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT) * + pWindow->alphaValue(WINDOW_ALPHA_ACTIVE) * PWORKSPACE->m_alpha->value(); if (A >= 1.f) { // if (PSURFACE->opaque) diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index fa1a8a061..81f7b723b 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -68,6 +68,7 @@ using namespace Hyprutils::Utils; using namespace Hyprutils::OS; using enum NContentType::eContentType; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render; extern "C" { @@ -234,8 +235,8 @@ bool IHyprRenderer::shouldRenderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor) { return true; // if the window is being moved to a workspace that is not invisible, and the alpha is > 0.F, render it. - if (pWindow->m_monitorMovedFrom != -1 && pWindow->m_movingToWorkspaceAlpha->isBeingAnimated() && pWindow->m_movingToWorkspaceAlpha->value() > 0.F && pWindow->m_workspace && - !pWindow->m_workspace->isVisible()) + if (pWindow->m_monitorMovedFrom != -1 && pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->isBeingAnimated() && pWindow->alphaValue(WINDOW_ALPHA_MOVE_TO_WORKSPACE) > 0.F && + pWindow->m_workspace && !pWindow->m_workspace->isVisible()) return true; const auto PWINDOWWORKSPACE = pWindow->m_workspace; @@ -244,7 +245,8 @@ bool IHyprRenderer::shouldRenderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor) { return true; // if hidden behind fullscreen - if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isFullscreen() && (!pWindow->m_isFloating || !pWindow->m_createdOverFullscreen) && pWindow->m_alpha->value() == 0) + if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isFullscreen() && (!pWindow->m_isFloating || !pWindow->m_createdOverFullscreen) && + pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) == 0) return false; if (!PWINDOWWORKSPACE->m_renderOffset->isBeingAnimated() && !PWINDOWWORKSPACE->m_alpha->isBeingAnimated() && !PWINDOWWORKSPACE->isVisible()) @@ -325,7 +327,7 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR if (!shouldRenderWindow(w, pMonitor)) continue; - if (w->m_alpha->value() == 0.f) + if (w->alphaValue(WINDOW_ALPHA_FADE) * w->alphaValue(WINDOW_ALPHA_FULLSCREEN) == 0.f) continue; if (w->isFullscreen()) @@ -569,9 +571,10 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T renderdata.surface = pWindow->wlSurface()->resource(); renderdata.dontRound = pWindow->isEffectiveInternalFSMode(FSMODE_FULLSCREEN); - renderdata.fadeAlpha = pWindow->m_alpha->value() * (pWindow->m_pinned || USE_WORKSPACE_FADE_ALPHA ? 1.f : PWORKSPACE->m_alpha->value()) * - (USE_WORKSPACE_FADE_ALPHA ? pWindow->m_movingToWorkspaceAlpha->value() : 1.F) * pWindow->m_movingFromWorkspaceAlpha->value(); - renderdata.alpha = pWindow->m_activeInactiveAlpha->value(); + renderdata.fadeAlpha = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT) * + (pWindow->m_pinned || USE_WORKSPACE_FADE_ALPHA ? 1.f : PWORKSPACE->m_alpha->value()) * + (USE_WORKSPACE_FADE_ALPHA ? pWindow->alphaValue(WINDOW_ALPHA_MOVE_TO_WORKSPACE) : 1.F) * pWindow->alphaValue(WINDOW_ALPHA_MOVE_FROM_WORKSPACE); + renderdata.alpha = pWindow->alphaValue(WINDOW_ALPHA_ACTIVE); renderdata.decorate = decorate && !pWindow->m_X11DoesntWantBorders && !pWindow->isEffectiveInternalFSMode(FSMODE_FULLSCREEN); renderdata.rounding = standalone || renderdata.dontRound ? 0 : pWindow->rounding() * pMonitor->m_scale; renderdata.roundingPower = standalone || renderdata.dontRound ? 2.0f : pWindow->roundingPower(); @@ -3145,8 +3148,9 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { if (*PDIMAROUND && pWindow->m_ruleApplicator->dimAround().valueOrDefault()) { CRectPassElement::SRectData data; - data.box = {0, 0, m_renderData.pMonitor->m_pixelSize.x, m_renderData.pMonitor->m_pixelSize.y}; - data.color = CHyprColor(0, 0, 0, *PDIMAROUND * pWindow->m_alpha->value()); + data.box = {0, 0, m_renderData.pMonitor->m_pixelSize.x, m_renderData.pMonitor->m_pixelSize.y}; + data.color = + CHyprColor(0, 0, 0, *PDIMAROUND * pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT)); m_renderPass.add(makeUnique(data)); } @@ -3156,7 +3160,8 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { data.box = CBox{pWindow->m_realPosition->value(), pWindow->m_realSize->value()}.translate(-PMONITOR->m_position).scale(PMONITOR->m_scale).round(); data.color = CHyprColor{0, 0, 0, 0}; data.blur = true; - data.blurA = sqrt(pWindow->m_alpha->value()); // sqrt makes the blur fadeout more realistic. + data.blurA = sqrt(pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * + pWindow->alphaValue(WINDOW_ALPHA_LAYOUT)); // sqrt makes the blur fadeout more realistic. data.round = pWindow->rounding(); data.roundingPower = pWindow->roundingPower(); data.xray = pWindow->m_ruleApplicator->xray().valueOr(false); @@ -3168,7 +3173,7 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { data.flipEndFrame = true; data.tex = FBDATA->getTexture(); data.box = windowBox; - data.a = pWindow->m_alpha->value(); + data.a = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT); data.damage = fakeDamage; m_renderPass.add(makeUnique(std::move(data))); From 55a1611fd8150d7bb8d1b6e2b8cc95ffccf31ea2 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Wed, 29 Apr 2026 17:25:21 +0100 Subject: [PATCH 06/48] desktop/window: expand hidden into proper states --- meta/hl.meta.lua | 2 + src/Compositor.cpp | 19 ++++---- src/config/lua/objects/LuaWindow.cpp | 4 ++ src/debug/HyprCtl.cpp | 35 ++++++++------- src/desktop/Workspace.cpp | 8 ++-- src/desktop/view/Group.cpp | 13 ++++-- src/desktop/view/Window.cpp | 45 ++++++++++++++++--- src/desktop/view/Window.hpp | 22 +++++++-- src/helpers/Monitor.cpp | 2 +- .../tiled/monocle/MonocleAlgorithm.cpp | 15 ++++--- src/render/OpenGL.cpp | 2 +- src/render/Renderer.cpp | 3 ++ 12 files changed, 122 insertions(+), 48 deletions(-) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index 4e31d4a34..ef2bf6168 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -667,6 +667,7 @@ local __HL_Notification = {} local __HL_Timer = {} ---@class HL.Window +---@field acceptsInput boolean ---@field active boolean|nil ---@field address string ---@field at integer|table @@ -692,6 +693,7 @@ local __HL_Timer = {} ---@field swallowing HL.Window|nil ---@field tags string|table ---@field title string +---@field visible boolean ---@field workspace HL.Workspace|nil ---@field xdg_description string|nil ---@field xdg_tag string|nil diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 3b7ea8154..19fe6d753 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -927,7 +927,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (ONLY_PRIORITY && !w->priorityFocus()) continue; - if (w->m_isFloating && w->m_isMapped && !w->isHidden() && !w->m_X11ShouldntFocus && w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && + if (w->m_isFloating && w->m_isMapped && w->acceptsInput() && !w->m_X11ShouldntFocus && w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { const auto BB = w->getWindowBoxUnified(properties); CBox box = BB.copy().expand(!w->isX11OverrideRedirect() ? BORDER_GRAB_AREA : 0); @@ -967,7 +967,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope continue; } - if (w->m_isFloating && w->m_isMapped && w->m_workspace->isVisible() && !w->isHidden() && !w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && + if (w->m_isFloating && w->m_isMapped && w->m_workspace->isVisible() && w->acceptsInput() && !w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && (!aboveFullscreen || w->m_createdOverFullscreen) && !isShadowedByModal(w)) { // OR windows should add focus to parent if (w->m_X11ShouldntFocus && !w->isX11OverrideRedirect()) @@ -1039,7 +1039,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (!w->m_workspace) continue; - if (!w->m_isX11 && !w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && !w->isHidden() && !w->m_X11ShouldntFocus && + if (!w->m_isX11 && !w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && w->acceptsInput() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { if (w->hasPopupAt(pos)) return w; @@ -1056,7 +1056,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (!w->m_workspace) continue; - if (!w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && !w->isHidden() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && + if (!w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && w->acceptsInput() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { CBox box = (properties & Desktop::View::USE_PROP_TILED) ? w->getWindowBoxUnified(properties) : CBox{w->m_position, w->m_size}; if ((properties & Desktop::View::INPUT_EXTENTS) && BORDER_GRAB_AREA > 0 && !w->isX11OverrideRedirect()) { @@ -1515,7 +1515,7 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks }; for (auto const& w : m_windows) { - if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible()) + if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || !w->acceptsInput() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible()) continue; if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) @@ -1594,7 +1594,7 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks constexpr float THRESHOLD = 0.3 * M_PI; for (auto const& w : m_windows) { - if (w == ignoreWindow || !w->m_isMapped || !w->m_workspace || w->isHidden() || (!w->isFullscreen() && !w->m_isFloating) || !w->m_workspace->isVisible()) + if (w == ignoreWindow || !w->m_isMapped || !w->m_workspace || !w->acceptsInput() || (!w->isFullscreen() && !w->m_isFloating) || !w->m_workspace->isVisible()) continue; if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) @@ -1642,7 +1642,8 @@ static bool isFloatingMatches(WINDOWPTR w, std::optional floating) { template static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional floating, bool anyWorkspace = false) { return isFloatingMatches(w, floating) && - (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && !w->isHidden() && (!focusableOnly || !w->m_ruleApplicator->noFocus().valueOrDefault())); + (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && w->acceptsInput() && + (!focusableOnly || !w->m_ruleApplicator->noFocus().valueOrDefault())); } template @@ -2331,7 +2332,7 @@ PHLWINDOW CCompositor::getWindowByRegex(const std::string& regexp_) { const bool FLOAT = regexp.starts_with("floating"); for (auto const& w : m_windows) { - if (!w->m_isMapped || w->m_isFloating != FLOAT || w->m_workspace != Desktop::focusState()->window()->m_workspace || w->isHidden()) + if (!w->m_isMapped || w->m_isFloating != FLOAT || w->m_workspace != Desktop::focusState()->window()->m_workspace || !w->acceptsInput()) continue; return w; @@ -2708,7 +2709,7 @@ void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWor PHLWINDOW CCompositor::getForceFocus() { for (auto const& w : m_windows) { - if (!w->m_isMapped || w->isHidden() || !w->m_workspace || !w->m_workspace->isVisible()) + if (!w->m_isMapped || !w->acceptsInput() || !w->m_workspace || !w->m_workspace->isVisible()) continue; if (!w->m_ruleApplicator->stayFocused().valueOrDefault()) diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp index bf3325845..b1a71aaa8 100644 --- a/src/config/lua/objects/LuaWindow.cpp +++ b/src/config/lua/objects/LuaWindow.cpp @@ -71,6 +71,10 @@ static int windowIndex(lua_State* L) { lua_pushboolean(L, w->m_isMapped); else if (key == "hidden") lua_pushboolean(L, w->isHidden()); + else if (key == "visible") + lua_pushboolean(L, w->visible()); + else if (key == "acceptsInput") + lua_pushboolean(L, w->acceptsInput()); else if (key == "at") { lua_newtable(L); lua_pushinteger(L, sc(w->m_realPosition->goal().x)); diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index 540a28e25..a3f5f9e00 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -382,6 +382,8 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) { "address": "0x{:x}", "mapped": {}, "hidden": {}, + "visible": {}, + "acceptsInput": {}, "at": [{}, {}], "size": [{}, {}], "workspace": {{ @@ -410,28 +412,31 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) { "contentType": "{}", "stableId": "{:x}" }},)#", - rc(w.get()), (w->m_isMapped ? "true" : "false"), (w->isHidden() ? "true" : "false"), sc(w->m_realPosition->goal().x), - sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, - escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), (sc(w->m_isFloating) == 1 ? "true" : "false"), w->monitorID(), escapeJSONStrings(w->m_class), - escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), escapeJSONStrings(w->m_initialTitle), w->getPID(), (sc(w->m_isX11) == 1 ? "true" : "false"), - (w->m_pinned ? "true" : "false"), sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), (w->m_createdOverFullscreen ? "true" : "false"), - getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), - (g_pInputManager->isWindowInhibiting(w, false) ? "true" : "false"), escapeJSONStrings(w->xdgTag().value_or("")), escapeJSONStrings(w->xdgDescription().value_or("")), - escapeJSONStrings(NContentType::toString(w->getContentType())), w->m_stableID); + rc(w.get()), (w->m_isMapped ? "true" : "false"), (w->isHidden() ? "true" : "false"), (w->visible() ? "true" : "false"), + (w->acceptsInput() ? "true" : "false"), sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), + sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), + (sc(w->m_isFloating) == 1 ? "true" : "false"), w->monitorID(), escapeJSONStrings(w->m_class), escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), + escapeJSONStrings(w->m_initialTitle), w->getPID(), (sc(w->m_isX11) == 1 ? "true" : "false"), (w->m_pinned ? "true" : "false"), + sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), (w->m_createdOverFullscreen ? "true" : "false"), getGroupedData(w, format), + getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), (g_pInputManager->isWindowInhibiting(w, false) ? "true" : "false"), + escapeJSONStrings(w->xdgTag().value_or("")), escapeJSONStrings(w->xdgDescription().value_or("")), escapeJSONStrings(NContentType::toString(w->getContentType())), + w->m_stableID); } else { return std::format( - "Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tmonitor: {}\n\tclass: {}\n\ttitle: " + "Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tvisible: {}\n\tacceptsInput: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tmonitor: " + "{}\n\tclass: {}\n\ttitle: " "{}\n\tinitialClass: {}\n\tinitialTitle: {}\n\tpid: " "{}\n\txwayland: {}\n\tpinned: " "{}\n\tfullscreen: {}\n\tfullscreenClient: {}\n\toverFullscreen: {}\n\tgrouped: {}\n\ttags: {}\n\tswallowing: {:x}\n\tfocusHistoryID: {}\n\tinhibitingIdle: " "{}\n\txdgTag: " "{}\n\txdgDescription: {}\n\tcontentType: {}\n\tstableID: {:x}\n\n", - rc(w.get()), w->m_title, sc(w->m_isMapped), sc(w->isHidden()), sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), - sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, - (!w->m_workspace ? "" : w->m_workspace->m_name), sc(w->m_isFloating), w->monitorID(), w->m_class, w->m_title, w->m_initialClass, w->m_initialTitle, w->getPID(), - sc(w->m_isX11), sc(w->m_pinned), sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), sc(w->m_createdOverFullscreen), - getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), sc(g_pInputManager->isWindowInhibiting(w, false)), - w->xdgTag().value_or(""), w->xdgDescription().value_or(""), NContentType::toString(w->getContentType()), w->m_stableID); + rc(w.get()), w->m_title, sc(w->m_isMapped), sc(w->isHidden()), sc(w->visible()), sc(w->acceptsInput()), + sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), + w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, (!w->m_workspace ? "" : w->m_workspace->m_name), sc(w->m_isFloating), w->monitorID(), w->m_class, + w->m_title, w->m_initialClass, w->m_initialTitle, w->getPID(), sc(w->m_isX11), sc(w->m_pinned), sc(w->m_fullscreenState.internal), + sc(w->m_fullscreenState.client), sc(w->m_createdOverFullscreen), getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), + getFocusHistoryID(w), sc(g_pInputManager->isWindowInhibiting(w, false)), w->xdgTag().value_or(""), w->xdgDescription().value_or(""), + NContentType::toString(w->getContentType()), w->m_stableID); } } diff --git a/src/desktop/Workspace.cpp b/src/desktop/Workspace.cpp index cb2da6b3c..e6f0c3061 100644 --- a/src/desktop/Workspace.cpp +++ b/src/desktop/Workspace.cpp @@ -426,7 +426,7 @@ int CWorkspace::getWindows(std::optional onlyTiled, std::optional on continue; if (onlyPinned.has_value() && (!t->window() || t->window()->m_pinned != onlyPinned.value())) continue; - if (onlyVisible.has_value() && (!t->window() || t->window()->isHidden() == onlyVisible.value())) + if (onlyVisible.has_value() && (!t->window() || t->window()->visible() != onlyVisible.value())) continue; no++; } @@ -445,7 +445,7 @@ int CWorkspace::getGroups(std::optional onlyTiled, std::optional onl continue; if (onlyPinned.has_value() && HEAD->m_pinned != onlyPinned.value()) continue; - if (onlyVisible.has_value() && g->current()->isHidden() == onlyVisible.value()) + if (onlyVisible.has_value() && g->current()->visible() != onlyVisible.value()) continue; no++; } @@ -454,7 +454,7 @@ int CWorkspace::getGroups(std::optional onlyTiled, std::optional onl PHLWINDOW CWorkspace::getFirstWindow() { for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace == m_self && w->m_isMapped && !w->isHidden()) + if (w->m_workspace == m_self && w->m_isMapped && w->acceptsInput()) return w; } @@ -465,7 +465,7 @@ PHLWINDOW CWorkspace::getTopLeftWindow() { const auto PMONITOR = m_monitor.lock(); for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace != m_self || !w->m_isMapped || w->isHidden()) + if (w->m_workspace != m_self || !w->m_isMapped || !w->acceptsInput()) continue; const auto WINDOWIDEALBB = w->getWindowIdealBoundingBoxIgnoreReserved(); diff --git a/src/desktop/view/Group.cpp b/src/desktop/view/Group.cpp index 464a38da2..f2b2a5642 100644 --- a/src/desktop/view/Group.cpp +++ b/src/desktop/view/Group.cpp @@ -142,7 +142,8 @@ void CGroup::remove(PHLWINDOW w, Math::eDirection dir) { w->m_group.reset(); removeWindowDecos(w); - w->setHidden(false); + w->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, false); + *w->alpha(WINDOW_ALPHA_LAYOUT) = 1.F; const bool REMOVING_GROUP = m_windows.size() <= 1; @@ -280,12 +281,16 @@ void CGroup::updateWindowVisibility() { for (size_t i = 0; i < m_windows.size(); ++i) { if (i == m_current) { auto& x = m_windows.at(i); - x->setHidden(false); + x->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, false); + *x->alpha(WINDOW_ALPHA_LAYOUT) = 1.F; x->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_GROUP | Desktop::Rule::RULE_PROP_ON_WORKSPACE); x->updateWindowDecos(); x->updateDecorationValues(); - } else - m_windows.at(i)->setHidden(true); + } else { + auto& x = m_windows.at(i); + x->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, true); + *x->alpha(WINDOW_ALPHA_LAYOUT) = 0.F; + } } m_target->recalc(); diff --git a/src/desktop/view/Window.cpp b/src/desktop/view/Window.cpp index ef854bd13..73c4378dc 100644 --- a/src/desktop/view/Window.cpp +++ b/src/desktop/view/Window.cpp @@ -87,7 +87,7 @@ PHLWINDOW CWindow::create(SP surface) { g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); @@ -121,7 +121,7 @@ PHLWINDOW CWindow::create(SP resource) { g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); @@ -187,7 +187,7 @@ eViewType CWindow::type() const { } bool CWindow::visible() const { - return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || (m_fadingOut && alphaValue(WINDOW_ALPHA_FADE) != 0.F)); + return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || m_fadingOut) && visibleByAlpha(); } std::optional CWindow::logicalBox() const { @@ -723,10 +723,37 @@ void CWindow::setHidden(bool hidden) { setSuspended(hidden); } -bool CWindow::isHidden() { +bool CWindow::isHidden() const { return m_hidden; } +void CWindow::setInputBlocked(eWindowInputBlockReason reason, bool blocked) { + if (reason == INPUT_BLOCK_NONE) + return; + + const auto MASK = sc(reason); + + if (blocked) + m_inputBlockReasons |= MASK; + else + m_inputBlockReasons &= ~MASK; + + if (blocked && Desktop::focusState()->window() == m_self) + Desktop::focusState()->window().reset(); +} + +bool CWindow::isInputBlocked() const { + return m_inputBlockReasons != INPUT_BLOCK_NONE; +} + +bool CWindow::isInputBlocked(eWindowInputBlockReason reason) const { + return (m_inputBlockReasons & sc(reason)) != 0; +} + +bool CWindow::acceptsInput() const { + return !isHidden() && !isInputBlocked(); +} + PHLANIMVAR& CWindow::alpha(eWindowAlpha type) { return m_alpha.get(type); } @@ -751,6 +778,14 @@ float CWindow::alphaTotalWithout(eWindowAlpha type) const { return m_alpha.getTotalWithout(type); } +float CWindow::effectiveAlpha() const { + return alphaTotal(); +} + +bool CWindow::visibleByAlpha() const { + return effectiveAlpha() != 0.F; +} + // check if the point is "hidden" under a rounded corner of the window // it is assumed that the point is within the real window box (m_vRealPosition, m_vRealSize) // otherwise behaviour is undefined @@ -1341,7 +1376,7 @@ PHLWINDOW CWindow::getSwallower() { break; for (auto const& w : g_pCompositor->m_windows) { - if (!w->m_isMapped || w->isHidden()) + if (!w->m_isMapped || !w->acceptsInput()) continue; if (w->getPID() == currentPid) diff --git a/src/desktop/view/Window.hpp b/src/desktop/view/Window.hpp index 0a468ee33..a5d89c58b 100644 --- a/src/desktop/view/Window.hpp +++ b/src/desktop/view/Window.hpp @@ -89,6 +89,13 @@ namespace Desktop::View { WINDOW_ALPHA_LAST, }; + enum eWindowInputBlockReason : uint32_t { + INPUT_BLOCK_NONE = 0, + INPUT_BLOCK_GROUP_INACTIVE = 1 << 0, + INPUT_BLOCK_MONOCLE_INACTIVE = 1 << 1, + INPUT_BLOCK_BELOW_FULLSCREEN = 1 << 2, + }; + struct SWindowActiveEvent { PHLWINDOW window = nullptr; eFocusReason reason = sc(0) /* unknown */; @@ -304,13 +311,19 @@ namespace Desktop::View { void onUnmap(); void onMap(); void setHidden(bool hidden); - bool isHidden(); + bool isHidden() const; + void setInputBlocked(eWindowInputBlockReason reason, bool blocked); + bool isInputBlocked() const; + bool isInputBlocked(eWindowInputBlockReason reason) const; + bool acceptsInput() const; PHLANIMVAR& alpha(eWindowAlpha type); const PHLANIMVAR& alpha(eWindowAlpha type) const; float alphaValue(eWindowAlpha type) const; float alphaGoal(eWindowAlpha type) const; float alphaTotal() const; float alphaTotalWithout(eWindowAlpha type) const; + float effectiveAlpha() const; + bool visibleByAlpha() const; void updateDecorationValues(); SBoxExtents getFullWindowReservedArea(); Vector2D middle(); @@ -414,9 +427,10 @@ namespace Desktop::View { void unmanagedSetGeometry(); // For hidden windows and stuff - bool m_hidden = false; - bool m_suspended = false; - WORKSPACEID m_lastWorkspace = WORKSPACE_INVALID; + bool m_hidden = false; + bool m_suspended = false; + WORKSPACEID m_lastWorkspace = WORKSPACE_INVALID; + uint32_t m_inputBlockReasons = INPUT_BLOCK_NONE; }; inline bool valid(PHLWINDOW w) { diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index 37606d4f6..96d80bf37 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -1755,7 +1755,7 @@ uint32_t CMonitor::isSolitaryBlocked(bool full) { } for (auto const& w : g_pCompositor->m_windows) { - if (w == PCANDIDATE || (!w->m_isMapped && !w->m_fadingOut) || w->isHidden()) + if (w == PCANDIDATE || (!w->m_isMapped && !w->m_fadingOut) || !w->visible()) continue; if (w->workspaceID() == PCANDIDATE->workspaceID() && w->m_isFloating && w->m_createdOverFullscreen && w->visibleOnMonitor(m_self.lock())) { diff --git a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp index 36c59a4ea..84b4f9e2c 100644 --- a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp +++ b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp @@ -46,8 +46,10 @@ CMonocleAlgorithm::~CMonocleAlgorithm() { continue; const auto WINDOW = TARGET->window(); - if (WINDOW) - WINDOW->setHidden(false); + if (WINDOW) { + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F; + } } m_focusCallback.reset(); @@ -81,8 +83,10 @@ void CMonocleAlgorithm::removeTarget(SP target) { // unhide window when removing from monocle layout const auto WINDOW = target->window(); - if (WINDOW) - WINDOW->setHidden(false); + if (WINDOW) { + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F; + } const auto INDEX = std::distance(m_targetDatas.begin(), it); m_targetDatas.erase(it); @@ -142,7 +146,8 @@ void CMonocleAlgorithm::recalculate() { TARGET->setPositionGlobal(WORK_AREA); const bool SHOULD_BE_VISIBLE = ((int)i == m_currentVisibleIndex); - WINDOW->setHidden(!SHOULD_BE_VISIBLE); + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, !SHOULD_BE_VISIBLE); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = SHOULD_BE_VISIBLE ? 1.F : 0.F; } } diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index 08c50de10..6bb04cfae 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -1890,7 +1890,7 @@ void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { bool hasWindows = false; for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace == pMonitor->m_activeWorkspace && !w->isHidden() && w->m_isMapped && (!w->m_isFloating || *PBLURXRAY)) { + if (w->m_workspace == pMonitor->m_activeWorkspace && w->visible() && w->m_isMapped && (!w->m_isFloating || *PBLURXRAY)) { // check if window is valid if (!windowShouldBeBlurred(w)) diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 81f7b723b..48405c3d5 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -529,6 +529,9 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T if (pWindow->isHidden() && !standalone) return; + if (!standalone && pWindow->effectiveAlpha() == 0.F && !pWindow->m_alpha.isBeingAnimated()) + return; + if (pWindow->m_fadingOut) { if (pMonitor == pWindow->m_monitor) // TODO: fix this renderSnapshot(pWindow); From 31e025dbe04dbe1836ea759f6b6e2ea09591fb23 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Wed, 29 Apr 2026 18:51:19 +0100 Subject: [PATCH 07/48] desktop/window: refactor over fullscreen state --- src/Compositor.cpp | 38 ++++++++++---- src/Compositor.hpp | 6 ++- src/config/lua/objects/LuaWindow.cpp | 2 +- src/config/shared/actions/ConfigActions.cpp | 6 ++- src/desktop/Workspace.cpp | 4 +- src/desktop/state/FocusState.cpp | 1 + src/desktop/view/Window.cpp | 50 ++++++++++++++++++- src/desktop/view/Window.hpp | 11 +++- src/helpers/Monitor.cpp | 2 +- src/layout/LayoutManager.cpp | 2 +- src/layout/supplementary/DragController.cpp | 2 +- .../animation/DesktopAnimationManager.cpp | 9 ++-- src/managers/input/InputManager.cpp | 4 +- src/render/Renderer.cpp | 9 ++-- 14 files changed, 113 insertions(+), 33 deletions(-) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 19fe6d753..14a7843ec 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -968,7 +968,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope } if (w->m_isFloating && w->m_isMapped && w->m_workspace->isVisible() && w->acceptsInput() && !w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && - w != pIgnoreWindow && (!aboveFullscreen || w->m_createdOverFullscreen) && !isShadowedByModal(w)) { + w != pIgnoreWindow && (!aboveFullscreen || w->isAllowedOverFullscreen()) && !isShadowedByModal(w)) { // OR windows should add focus to parent if (w->m_X11ShouldntFocus && !w->isX11OverrideRedirect()) continue; @@ -1310,6 +1310,9 @@ void CCompositor::changeWindowZOrder(PHLWINDOW pWindow, bool top) { else pWindow->m_createdOverFullscreen = false; + pWindow->updateFullscreenInputState(); + *pWindow->alpha(WINDOW_ALPHA_FULLSCREEN) = pWindow->isBlockedByFullscreen() ? 0.F : 1.F; + if (pWindow == (top ? m_windows.back() : m_windows.front())) return; @@ -1521,7 +1524,7 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) continue; - if (pWorkspace->m_hasFullscreenWindow && !w->isFullscreen() && !w->m_createdOverFullscreen) + if (pWorkspace->m_hasFullscreenWindow && !w->isAllowedOverFullscreen()) continue; if (!*PMONITORFALLBACK && pWorkspace->m_monitor != w->m_monitor) @@ -1600,7 +1603,7 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) continue; - if (pWorkspace->m_hasFullscreenWindow && !w->isFullscreen() && !w->m_createdOverFullscreen) + if (pWorkspace->m_hasFullscreenWindow && !w->isAllowedOverFullscreen()) continue; if (!*PMONITORFALLBACK && pWorkspace->m_monitor != w->m_monitor) @@ -1640,9 +1643,18 @@ static bool isFloatingMatches(WINDOWPTR w, std::optional floating) { } template -static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional floating, bool anyWorkspace = false) { +static bool acceptsInputForCycle(WINDOWPTR w, bool allowFullscreenBlocked) { + if (w->acceptsInput()) + return true; + + return allowFullscreenBlocked && !w->isHidden() && w->isInputBlockedOnly(INPUT_BLOCK_BELOW_FULLSCREEN); +} + +template +static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional floating, bool anyWorkspace = false, + bool allowFullscreenBlocked = false) { return isFloatingMatches(w, floating) && - (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && w->acceptsInput() && + (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && acceptsInputForCycle(w, allowFullscreenBlocked) && (!focusableOnly || !w->m_ruleApplicator->noFocus().valueOrDefault())); } @@ -1664,16 +1676,16 @@ static PHLWINDOW getWeakWindowPred(Iterator cur, Iterator end, Iterator begin, c return IN_OTHER_SIDE->lock(); } -PHLWINDOW CCompositor::getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly, std::optional floating, bool visible, bool next) { - const auto FINDER = [&](const PHLWINDOWREF& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible); }; +PHLWINDOW CCompositor::getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly, std::optional floating, bool visible, bool next, bool allowFullscreenBlocked) { + const auto FINDER = [&](const PHLWINDOWREF& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible, allowFullscreenBlocked); }; // also m_vWindowFocusHistory has reverse order, so when it is next - we need to reverse again const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); return next ? getWeakWindowPred(std::ranges::find(HISTORY, cur), HISTORY.end(), HISTORY.begin(), FINDER) : getWeakWindowPred(std::ranges::find(HISTORY | std::views::reverse, cur), HISTORY.rend(), HISTORY.rbegin(), FINDER); } -PHLWINDOW CCompositor::getWindowCycle(PHLWINDOW cur, bool focusableOnly, std::optional floating, bool visible, bool prev) { - const auto FINDER = [&](const PHLWINDOW& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible); }; +PHLWINDOW CCompositor::getWindowCycle(PHLWINDOW cur, bool focusableOnly, std::optional floating, bool visible, bool prev, bool allowFullscreenBlocked) { + const auto FINDER = [&](const PHLWINDOW& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible, allowFullscreenBlocked); }; return prev ? getWindowPred(std::ranges::find(m_windows | std::views::reverse, cur), m_windows.rend(), m_windows.rbegin(), FINDER) : getWindowPred(std::ranges::find(m_windows, cur), m_windows.end(), m_windows.begin(), FINDER); } @@ -2259,8 +2271,12 @@ void CCompositor::setWindowFullscreenState(const PHLWINDOW PWINDOW, Desktop::Vie // make all windows and layers on the same workspace under the fullscreen window for (auto const& w : m_windows) { - if (w->m_workspace == PWORKSPACE && !w->isFullscreen() && !w->m_fadingOut && !w->m_pinned) - w->m_createdOverFullscreen = false; + if (w->m_workspace == PWORKSPACE) { + if (!w->isFullscreen() && !w->m_fadingOut && !w->m_pinned) + w->m_createdOverFullscreen = false; + + w->updateFullscreenInputState(); + } } for (auto const& ls : m_layers) { if (ls->m_monitor == PMONITOR) diff --git a/src/Compositor.hpp b/src/Compositor.hpp index 8f2db1a5d..7fbc7d525 100644 --- a/src/Compositor.hpp +++ b/src/Compositor.hpp @@ -117,8 +117,10 @@ class CCompositor { void cleanupFadingOut(const MONITORID& monid); PHLWINDOW getWindowInDirection(PHLWINDOW, Math::eDirection); PHLWINDOW getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, Math::eDirection dir, PHLWINDOW ignoreWindow = nullptr, bool useVectorAngles = false); - PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool prev = false); - PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool next = false); + PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool prev = false, + bool allowFullscreenBlocked = false); + PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool next = false, + bool allowFullscreenBlocked = false); WORKSPACEID getNextAvailableNamedWorkspace(); bool isPointOnAnyMonitor(const Vector2D&); bool isPointOnReservedArea(const Vector2D& point, const PHLMONITOR monitor = nullptr); diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp index b1a71aaa8..1d9334626 100644 --- a/src/config/lua/objects/LuaWindow.cpp +++ b/src/config/lua/objects/LuaWindow.cpp @@ -73,7 +73,7 @@ static int windowIndex(lua_State* L) { lua_pushboolean(L, w->isHidden()); else if (key == "visible") lua_pushboolean(L, w->visible()); - else if (key == "acceptsInput") + else if (key == "accepts_input") lua_pushboolean(L, w->acceptsInput()); else if (key == "at") { lua_newtable(L); diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index c36a149f8..1664df181 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -230,6 +230,8 @@ ActionResult Actions::pinWindow(eTogglableAction action, std::optionalm_pinned = wantPin; + window->updateFullscreenInputState(); + *window->alpha(Desktop::View::WINDOW_ALPHA_FULLSCREEN) = window->isBlockedByFullscreen() ? 0.F : 1.F; const auto PMONITOR = window->m_monitor.lock(); if (!PMONITOR) @@ -349,7 +351,7 @@ ActionResult Actions::moveFocus(Math::eDirection dir) { } const auto PWINDOWTOCHANGETO = *PFULLCYCLE && PLASTWINDOW->isFullscreen() ? - g_pCompositor->getWindowCycle(PLASTWINDOW, true, {}, false, dir != Math::DIRECTION_DOWN && dir != Math::DIRECTION_RIGHT) : + g_pCompositor->getWindowCycle(PLASTWINDOW, true, {}, false, dir != Math::DIRECTION_DOWN && dir != Math::DIRECTION_RIGHT, true) : g_pCompositor->getWindowInDirection(PLASTWINDOW, dir); if (*PGROUPCYCLE && PLASTWINDOW->m_group) { @@ -1651,7 +1653,7 @@ ActionResult Actions::cycleNext(const bool next, std::optional onlyTiled, if (onlyFloating.value_or(false)) floatStatus = true; - const auto& cycled = g_pCompositor->getWindowCycle(window, true, floatStatus, false, !next); + const auto& cycled = g_pCompositor->getWindowCycle(window, true, floatStatus, false, !next, window->m_workspace && window->m_workspace->m_hasFullscreenWindow); switchToWindow(cycled); diff --git a/src/desktop/Workspace.cpp b/src/desktop/Workspace.cpp index e6f0c3061..6d8e6cb49 100644 --- a/src/desktop/Workspace.cpp +++ b/src/desktop/Workspace.cpp @@ -426,7 +426,7 @@ int CWorkspace::getWindows(std::optional onlyTiled, std::optional on continue; if (onlyPinned.has_value() && (!t->window() || t->window()->m_pinned != onlyPinned.value())) continue; - if (onlyVisible.has_value() && (!t->window() || t->window()->visible() != onlyVisible.value())) + if (onlyVisible.has_value() && (!t->window() || t->window()->targetVisible() != onlyVisible.value())) continue; no++; } @@ -445,7 +445,7 @@ int CWorkspace::getGroups(std::optional onlyTiled, std::optional onl continue; if (onlyPinned.has_value() && HEAD->m_pinned != onlyPinned.value()) continue; - if (onlyVisible.has_value() && g->current()->visible() != onlyVisible.value()) + if (onlyVisible.has_value() && g->current()->targetVisible() != onlyVisible.value()) continue; no++; } diff --git a/src/desktop/state/FocusState.cpp b/src/desktop/state/FocusState.cpp index 4658a32ec..5db4bac5c 100644 --- a/src/desktop/state/FocusState.cpp +++ b/src/desktop/state/FocusState.cpp @@ -37,6 +37,7 @@ static SFullscreenWorkspaceFocusResult onFullscreenWorkspaceFocusWindow(PHLWINDO if (pWindow->m_isFloating) { // if the window is floating, just bring it to the top pWindow->m_createdOverFullscreen = true; + pWindow->updateFullscreenInputState(); g_pDesktopAnimationManager->setFullscreenFloatingFade(pWindow, 1.f); g_pHyprRenderer->damageWindow(pWindow); return {}; diff --git a/src/desktop/view/Window.cpp b/src/desktop/view/Window.cpp index 73c4378dc..b2f4821b8 100644 --- a/src/desktop/view/Window.cpp +++ b/src/desktop/view/Window.cpp @@ -512,6 +512,8 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) { } m_workspace = pWorkspace; + updateFullscreenInputState(); + *alpha(WINDOW_ALPHA_FULLSCREEN) = isBlockedByFullscreen() ? 0.F : 1.F; setAnimationsToMove(); @@ -750,10 +752,44 @@ bool CWindow::isInputBlocked(eWindowInputBlockReason reason) const { return (m_inputBlockReasons & sc(reason)) != 0; } +bool CWindow::isInputBlockedOnly(eWindowInputBlockReason reason) const { + return m_inputBlockReasons == sc(reason); +} + bool CWindow::acceptsInput() const { return !isHidden() && !isInputBlocked(); } +bool CWindow::isAllowedOverFullscreen() const { + if (isFullscreen() || m_pinned || m_createdOverFullscreen) + return true; + + if (!m_workspace) + return false; + + const auto FSWINDOW = m_workspace->getFullscreenWindow(); + return FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(m_self.lock()); +} + +bool CWindow::isBlockedByFullscreen() const { + if (!m_workspace || !m_workspace->m_hasFullscreenWindow) + return false; + + return !isAllowedOverFullscreen(); +} + +bool CWindow::isFadingOutUnderFullscreen() const { + return isBlockedByFullscreen() && alpha(WINDOW_ALPHA_FULLSCREEN)->isBeingAnimated() && alphaValue(WINDOW_ALPHA_FULLSCREEN) > 0.F; +} + +bool CWindow::shouldRenderOverFullscreen() const { + return isAllowedOverFullscreen() || isFadingOutUnderFullscreen(); +} + +void CWindow::updateFullscreenInputState() { + setInputBlocked(INPUT_BLOCK_BELOW_FULLSCREEN, isBlockedByFullscreen()); +} + PHLANIMVAR& CWindow::alpha(eWindowAlpha type) { return m_alpha.get(type); } @@ -774,6 +810,10 @@ float CWindow::alphaTotal() const { return m_alpha.getTotal(); } +float CWindow::alphaTotalGoal() const { + return m_alpha.getTotalGoal(); +} + float CWindow::alphaTotalWithout(eWindowAlpha type) const { return m_alpha.getTotalWithout(type); } @@ -786,6 +826,14 @@ bool CWindow::visibleByAlpha() const { return effectiveAlpha() != 0.F; } +bool CWindow::visibleByAlphaGoal() const { + return alphaTotalGoal() != 0.F; +} + +bool CWindow::targetVisible() const { + return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || m_fadingOut) && visibleByAlphaGoal(); +} + // check if the point is "hidden" under a rounded corner of the window // it is assumed that the point is within the real window box (m_vRealPosition, m_vRealSize) // otherwise behaviour is undefined @@ -1040,7 +1088,7 @@ bool CWindow::clampWindowSize(const std::optional minSize, const std:: return changed; } -bool CWindow::isFullscreen() { +bool CWindow::isFullscreen() const { return m_fullscreenState.internal != FSMODE_NONE; } diff --git a/src/desktop/view/Window.hpp b/src/desktop/view/Window.hpp index a5d89c58b..32a7bcfe2 100644 --- a/src/desktop/view/Window.hpp +++ b/src/desktop/view/Window.hpp @@ -315,15 +315,24 @@ namespace Desktop::View { void setInputBlocked(eWindowInputBlockReason reason, bool blocked); bool isInputBlocked() const; bool isInputBlocked(eWindowInputBlockReason reason) const; + bool isInputBlockedOnly(eWindowInputBlockReason reason) const; bool acceptsInput() const; + bool isAllowedOverFullscreen() const; + bool isBlockedByFullscreen() const; + bool isFadingOutUnderFullscreen() const; + bool shouldRenderOverFullscreen() const; + void updateFullscreenInputState(); PHLANIMVAR& alpha(eWindowAlpha type); const PHLANIMVAR& alpha(eWindowAlpha type) const; float alphaValue(eWindowAlpha type) const; float alphaGoal(eWindowAlpha type) const; float alphaTotal() const; + float alphaTotalGoal() const; float alphaTotalWithout(eWindowAlpha type) const; float effectiveAlpha() const; bool visibleByAlpha() const; + bool visibleByAlphaGoal() const; + bool targetVisible() const; void updateDecorationValues(); SBoxExtents getFullWindowReservedArea(); Vector2D middle(); @@ -339,7 +348,7 @@ namespace Desktop::View { void activate(bool force = false); int surfacesCount(); bool clampWindowSize(const std::optional minSize, const std::optional maxSize); - bool isFullscreen(); + bool isFullscreen() const; bool isEffectiveInternalFSMode(const eFullscreenMode) const; int getRealBorderSize() const; float getScrollMouse(); diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index 96d80bf37..657a8c11b 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -1758,7 +1758,7 @@ uint32_t CMonitor::isSolitaryBlocked(bool full) { if (w == PCANDIDATE || (!w->m_isMapped && !w->m_fadingOut) || !w->visible()) continue; - if (w->workspaceID() == PCANDIDATE->workspaceID() && w->m_isFloating && w->m_createdOverFullscreen && w->visibleOnMonitor(m_self.lock())) { + if (w->workspaceID() == PCANDIDATE->workspaceID() && w->m_isFloating && w->isAllowedOverFullscreen() && w->visibleOnMonitor(m_self.lock())) { reasons |= SC_FLOAT; if (!full) return reasons; diff --git a/src/layout/LayoutManager.cpp b/src/layout/LayoutManager.cpp index 82c59d0d3..f90cedf63 100644 --- a/src/layout/LayoutManager.cpp +++ b/src/layout/LayoutManager.cpp @@ -217,7 +217,7 @@ void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SPm_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 || + if ((HASFULLSCREEN && !other->isAllowedOverFullscreen()) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut || other->isX11OverrideRedirect()) continue; diff --git a/src/layout/supplementary/DragController.cpp b/src/layout/supplementary/DragController.cpp index 305e27ecd..3def85966 100644 --- a/src/layout/supplementary/DragController.cpp +++ b/src/layout/supplementary/DragController.cpp @@ -48,7 +48,7 @@ bool CDragStateController::updateDragWindow() { const auto PWORKSPACE = DRAGGINGTARGET->workspace(); const auto DRAGGINGWINDOW = DRAGGINGTARGET->window(); - if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || (!DRAGGINGWINDOW->m_createdOverFullscreen && !DRAGGINGWINDOW->m_pinned))) { + if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || !DRAGGINGWINDOW->isAllowedOverFullscreen())) { Log::logger->log(Log::DEBUG, "Rejecting drag on a fullscreen workspace. (window under fullscreen)"); CKeybindManager::changeMouseBindMode(MBIND_INVALID); return true; diff --git a/src/managers/animation/DesktopAnimationManager.cpp b/src/managers/animation/DesktopAnimationManager.cpp index 3ef9ff009..2b7cb4590 100644 --- a/src/managers/animation/DesktopAnimationManager.cpp +++ b/src/managers/animation/DesktopAnimationManager.cpp @@ -467,10 +467,9 @@ void CDesktopAnimationManager::setFullscreenFadeAnimation(PHLWORKSPACE ws, eAnim const auto FULLSCREEN = type == ANIMATION_TYPE_IN; - const auto FSWINDOW = ws->getFullscreenWindow(); - for (auto const& w : g_pCompositor->m_windows) { if (w->m_workspace == ws) { + w->updateFullscreenInputState(); if (w->m_fadingOut || w->m_pinned || w->isFullscreen()) continue; @@ -478,9 +477,7 @@ void CDesktopAnimationManager::setFullscreenFadeAnimation(PHLWORKSPACE ws, eAnim if (!FULLSCREEN) *w->alpha(WINDOW_ALPHA_FULLSCREEN) = 1.F; else if (!w->isFullscreen()) { - const bool CREATED_OVER_FS = w->m_createdOverFullscreen; - const bool IS_IN_GROUP_OF_FS = FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(w); - *w->alpha(WINDOW_ALPHA_FULLSCREEN) = !CREATED_OVER_FS && !IS_IN_GROUP_OF_FS ? 0.f : 1.f; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = w->isAllowedOverFullscreen() ? 1.f : 0.f; } } } @@ -500,6 +497,7 @@ void CDesktopAnimationManager::setFullscreenFloatingFade(PHLWINDOW pWindow, floa return; *pWindow->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; + pWindow->updateFullscreenInputState(); } void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, float fade, PHLWINDOW exclude) { @@ -512,6 +510,7 @@ void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, flo continue; *w->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; + w->updateFullscreenInputState(); } } diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index f0314e0d7..7ed7b4f6b 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -447,7 +447,7 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st const auto& PWINDOWIDEAL = getWindowIdeal(); if (PWINDOWIDEAL && - ((PWINDOWIDEAL->m_isFloating && (PWINDOWIDEAL->m_createdOverFullscreen || PWINDOWIDEAL->m_pinned)) /* floating over fullscreen or pinned */ + ((PWINDOWIDEAL->m_isFloating && PWINDOWIDEAL->isAllowedOverFullscreen()) /* floating over fullscreen or pinned */ || (PMONITOR->m_activeSpecialWorkspace == PWINDOWIDEAL->m_workspace) /* on an open special workspace */)) pFoundWindow = PWINDOWIDEAL; @@ -485,7 +485,7 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st if (pFoundWindow != PWINDOWIDEAL) pFoundWindow = PWINDOWIDEAL; - if (!(pFoundWindow && (pFoundWindow->m_isFloating && (pFoundWindow->m_createdOverFullscreen || pFoundWindow->m_pinned)))) + if (!(pFoundWindow && (pFoundWindow->m_isFloating && pFoundWindow->isAllowedOverFullscreen()))) pFoundWindow = PWORKSPACE->getFullscreenWindow(); } } diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 48405c3d5..b5a32a2b9 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -245,7 +245,7 @@ bool IHyprRenderer::shouldRenderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor) { return true; // if hidden behind fullscreen - if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isFullscreen() && (!pWindow->m_isFloating || !pWindow->m_createdOverFullscreen) && + if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isAllowedOverFullscreen() && pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) == 0) return false; @@ -358,6 +358,9 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR if (pWorkspace->m_isSpecialWorkspace && w->m_monitor != pWorkspace->m_monitor) continue; // special on another are rendered as a part of the base pass + if (w->isFadingOutUnderFullscreen()) + continue; // render these over fullscreen so the fade-out is visible + renderWindow(w, pMonitor, time, true, RENDER_PASS_ALL); } @@ -396,8 +399,8 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR // then render windows over fullscreen. for (auto const& w : g_pCompositor->m_windows) { - const bool shouldSkipWindow = w->workspaceID() != pWorkspaceWindow->workspaceID() || !w->m_isFloating || (!w->m_createdOverFullscreen && !w->m_pinned) || - (!w->m_isMapped && !w->m_fadingOut) || w->isFullscreen(); + const bool shouldSkipWindow = + w->workspaceID() != pWorkspaceWindow->workspaceID() || !w->m_isFloating || !w->shouldRenderOverFullscreen() || (!w->m_isMapped && !w->m_fadingOut) || w->isFullscreen(); if (shouldSkipWindow) continue; From 56d7a43102b53f79a2311662783d5dd94cb2f1a5 Mon Sep 17 00:00:00 2001 From: ImperishableSecret <57067794+imperishableSecret@users.noreply.github.com> Date: Thu, 30 Apr 2026 02:43:15 +0530 Subject: [PATCH 08/48] config/lua: expand properties in the workspace object (#14194) --- meta/hl.meta.lua | 21 +++++- src/config/lua/LuaEventHandler.cpp | 4 +- src/config/lua/objects/LuaGroup.cpp | 83 +++++++++++++++++++++++ src/config/lua/objects/LuaGroup.hpp | 14 ++++ src/config/lua/objects/LuaWindow.cpp | 34 +--------- src/config/lua/objects/LuaWorkspace.cpp | 87 +++++++++++++++++++++++++ 6 files changed, 207 insertions(+), 36 deletions(-) create mode 100644 src/config/lua/objects/LuaGroup.cpp create mode 100644 src/config/lua/objects/LuaGroup.hpp diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index ef2bf6168..64f49b2ae 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -565,6 +565,15 @@ local __HL_WorkspaceRuleSpec = {} ---@field remove fun(self: HL.EventSubscription, ...): any local __HL_EventSubscription = {} +---@class HL.Group +---@field current HL.Window|nil +---@field current_index integer +---@field denied boolean +---@field locked boolean +---@field members HL.Window|table|nil +---@field size integer +local __HL_Group = {} + ---@class HL.Keybind ---@field is_enabled fun(self: HL.Keybind, ...): any ---@field remove fun(self: HL.Keybind, ...): any @@ -677,7 +686,7 @@ local __HL_Timer = {} ---@field focus_history_id integer ---@field fullscreen integer ---@field fullscreen_client integer ----@field group HL.Window|boolean|integer|table|nil +---@field group HL.Group|nil ---@field hidden boolean ---@field inhibiting_idle boolean ---@field initial_class string @@ -706,15 +715,23 @@ local __HL_Window = {} local __HL_WindowRule = {} ---@class HL.Workspace +---@field get_groups fun(self: HL.Workspace, ...): any +---@field get_windows fun(self: HL.Workspace, ...): any ---@field active boolean +---@field config_name string ---@field fullscreen_mode integer +---@field fullscreen_window HL.Window|nil +---@field groups integer|nil ---@field has_fullscreen boolean ---@field has_urgent boolean ---@field id integer ----@field is_persistent boolean|nil +---@field is_empty boolean +---@field is_persistent boolean +---@field last_window HL.Window|nil ---@field monitor HL.Monitor|nil ---@field name string ---@field special boolean +---@field tiled_layout string ---@field visible boolean ---@field windows integer local __HL_Workspace = {} diff --git a/src/config/lua/LuaEventHandler.cpp b/src/config/lua/LuaEventHandler.cpp index 4fa138dd8..eb4f6bc52 100644 --- a/src/config/lua/LuaEventHandler.cpp +++ b/src/config/lua/LuaEventHandler.cpp @@ -2,10 +2,10 @@ #include "ConfigManager.hpp" #include "objects/LuaWindow.hpp" #include "objects/LuaWorkspace.hpp" +#include "objects/LuaGroup.hpp" #include "objects/LuaMonitor.hpp" #include "objects/LuaLayerSurface.hpp" -#include "../../defines.hpp" #include "../../event/EventBus.hpp" #include "../../desktop/state/FocusState.hpp" @@ -14,7 +14,6 @@ extern "C" { } #include -#include using namespace Config::Lua; using namespace Config::Lua::Objects; @@ -78,6 +77,7 @@ void CLuaEventHandler::dispatch(const std::string& name, int nargs, const std::f CLuaEventHandler::CLuaEventHandler(lua_State* L) : m_lua(L) { CLuaWindow{}.setup(L); + Objects::CLuaGroup{}.setup(L); CLuaWorkspace{}.setup(L); CLuaMonitor{}.setup(L); CLuaLayerSurface{}.setup(L); diff --git a/src/config/lua/objects/LuaGroup.cpp b/src/config/lua/objects/LuaGroup.cpp new file mode 100644 index 000000000..551f8a915 --- /dev/null +++ b/src/config/lua/objects/LuaGroup.cpp @@ -0,0 +1,83 @@ +#include "LuaGroup.hpp" +#include "LuaWindow.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../desktop/view/Group.hpp" + +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Group"; + +static int groupEq(lua_State* L) { + const auto* lhs = sc*>(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc*>(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int groupToString(lua_State* L) { + const auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto group = ref->lock(); + + if (!group) + lua_pushstring(L, "HL.Group(expired)"); + else + lua_pushfstring(L, "HL.Group(%p)", group.get()); + + return 1; +} + +static int groupIndex(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto group = ref->lock(); + if (!group) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "locked") + lua_pushboolean(L, group->locked()); + else if (key == "denied") + lua_pushboolean(L, group->denied()); + else if (key == "size") + lua_pushinteger(L, sc(group->size())); + else if (key == "current_index") + lua_pushinteger(L, sc(group->getCurrentIdx()) + 1); + else if (key == "current") { + const auto current = group->current(); + if (current) + Objects::CLuaWindow::push(L, current); + else + lua_pushnil(L); + } else if (key == "members") { + lua_newtable(L); + int i = 1; + for (const auto& grouped : group->windows()) { + const auto groupedWindow = grouped.lock(); + if (!groupedWindow) + continue; + + Objects::CLuaWindow::push(L, groupedWindow); + lua_rawseti(L, -2, i++); + } + } else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaGroup::setup(lua_State* L) { + registerMetatable(L, MT, groupIndex, gcRef>, groupEq, groupToString); +} + +void Objects::CLuaGroup::push(lua_State* L, SP group) { + new (lua_newuserdata(L, sizeof(WP))) WP(group); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaGroup.hpp b/src/config/lua/objects/LuaGroup.hpp new file mode 100644 index 000000000..b7d638c6c --- /dev/null +++ b/src/config/lua/objects/LuaGroup.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +#include "../../../desktop/view/Group.hpp" + +namespace Config::Lua::Objects { + class CLuaGroup { + public: + static void setup(lua_State* L); + static void push(lua_State* L, SP group); + }; +}; diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp index 1d9334626..7fe87334d 100644 --- a/src/config/lua/objects/LuaWindow.cpp +++ b/src/config/lua/objects/LuaWindow.cpp @@ -1,6 +1,7 @@ #include "LuaWindow.hpp" #include "LuaWorkspace.hpp" #include "LuaMonitor.hpp" +#include "LuaGroup.hpp" #include "LuaObjectHelpers.hpp" #include "../../../desktop/view/Window.hpp" @@ -126,38 +127,7 @@ static int windowIndex(lua_State* L) { return 1; } - lua_newtable(L); - - lua_pushboolean(L, w->m_group->locked()); - lua_setfield(L, -2, "locked"); - - lua_pushboolean(L, w->m_group->denied()); - lua_setfield(L, -2, "denied"); - - lua_pushinteger(L, sc(w->m_group->size())); - lua_setfield(L, -2, "size"); - - lua_pushinteger(L, sc(w->m_group->getCurrentIdx()) + 1); - lua_setfield(L, -2, "current_index"); - - const auto current = w->m_group->current(); - if (current) - Objects::CLuaWindow::push(L, current); - else - lua_pushnil(L); - lua_setfield(L, -2, "current"); - - lua_newtable(L); - int i = 1; - for (const auto& grouped : w->m_group->windows()) { - const auto groupedWindow = grouped.lock(); - if (!groupedWindow) - continue; - - Objects::CLuaWindow::push(L, groupedWindow); - lua_rawseti(L, -2, i++); - } - lua_setfield(L, -2, "members"); + Objects::CLuaGroup::push(L, w->m_group); } else if (key == "tags") { lua_newtable(L); diff --git a/src/config/lua/objects/LuaWorkspace.cpp b/src/config/lua/objects/LuaWorkspace.cpp index 84e4df0b4..3f89b41a1 100644 --- a/src/config/lua/objects/LuaWorkspace.cpp +++ b/src/config/lua/objects/LuaWorkspace.cpp @@ -1,10 +1,19 @@ #include "LuaWorkspace.hpp" #include "LuaMonitor.hpp" +#include "LuaWindow.hpp" +#include "LuaGroup.hpp" #include "LuaObjectHelpers.hpp" #include "../../../desktop/Workspace.hpp" +#include "../../../desktop/view/Group.hpp" #include "../../../helpers/Monitor.hpp" +#include "../../../layout/space/Space.hpp" +#include "../../../layout/algorithm/Algorithm.hpp" +#include "../../../layout/algorithm/TiledAlgorithm.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../../Compositor.hpp" +#include #include using namespace Config::Lua; @@ -32,6 +41,55 @@ static int workspaceToString(lua_State* L) { return 1; } +static int workspaceGetWindows(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + lua_newtable(L); + return 1; + } + + lua_newtable(L); + int idx = 1; + for (auto const& w : g_pCompositor->m_windows) { + if (w->m_workspace == ws) { + Objects::CLuaWindow::push(L, w); + lua_rawseti(L, -2, idx++); + } + } + return 1; +} + +static int workspaceGetGroups(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + lua_newtable(L); + return 1; + } + + lua_newtable(L); + int idx = 1; + + std::vector pushedGroups; + + for (auto const& w : g_pCompositor->m_windows) { + if (w->m_workspace != ws || !w->m_group) + continue; + + if (std::ranges::find(pushedGroups, w->m_group.get()) != pushedGroups.end()) + continue; + + pushedGroups.push_back(w->m_group.get()); + + Objects::CLuaGroup::push(L, w->m_group); + + lua_rawseti(L, -2, idx++); + } + + return 1; +} + static int workspaceIndex(lua_State* L) { auto* ref = sc(luaL_checkudata(L, 1, MT)); const auto ws = ref->lock(); @@ -70,6 +128,35 @@ static int workspaceIndex(lua_State* L) { lua_pushboolean(L, ws->m_hasFullscreenWindow); else if (key == "is_persistent") lua_pushboolean(L, ws->isPersistent()); + else if (key == "is_empty") + lua_pushboolean(L, ws->getWindows() == 0); + else if (key == "config_name") + lua_pushstring(L, ws->getConfigName().c_str()); + else if (key == "tiled_layout") { + std::string layoutName = "unknown"; + if (ws->m_space && ws->m_space->algorithm() && ws->m_space->algorithm()->tiledAlgo()) { + const auto& TILED_ALGO = ws->m_space->algorithm()->tiledAlgo(); + layoutName = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*TILED_ALGO.get())); + } + lua_pushstring(L, layoutName.c_str()); + } else if (key == "last_window") { + const auto lastWindow = ws->m_lastFocusedWindow.lock(); + if (lastWindow) + Objects::CLuaWindow::push(L, lastWindow); + else + lua_pushnil(L); + } else if (key == "fullscreen_window") { + const auto fsWindow = ws->getFullscreenWindow(); + if (fsWindow) + Objects::CLuaWindow::push(L, fsWindow); + else + lua_pushnil(L); + } else if (key == "get_windows") + lua_pushcfunction(L, workspaceGetWindows); + else if (key == "get_groups") + lua_pushcfunction(L, workspaceGetGroups); + else if (key == "groups") + lua_pushinteger(L, sc(ws->getGroups())); else lua_pushnil(L); From 2ff598896d3334cb44331463e845128cae4815f1 Mon Sep 17 00:00:00 2001 From: Mihai Fufezan Date: Wed, 29 Apr 2026 17:20:14 +0300 Subject: [PATCH 09/48] CI/Nix/Test: check gtest exit status --- .github/workflows/nix-test.yml | 5 ++++- nix/tests/default.nix | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nix-test.yml b/.github/workflows/nix-test.yml index 666d971c1..2a47656cd 100644 --- a/.github/workflows/nix-test.yml +++ b/.github/workflows/nix-test.yml @@ -18,7 +18,10 @@ jobs: run: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}#checks.x86_64-linux.tests' -L --extra-substituters "https://hyprland.cachix.org" - name: Check exit status - run: grep 0 result/exit_status + run: | + grep 0 result/exit_status || echo "hyprtester failed" + grep 0 result/exit_status_gtests || echo "gtests failed" + [ 0 = $(cat result/exit_status) ] && [ 0 = $(cat result/exit_status_gtests) ] - name: Upload artifacts if: always() diff --git a/nix/tests/default.nix b/nix/tests/default.nix index 7ed0efb50..6c8e54090 100644 --- a/nix/tests/default.nix +++ b/nix/tests/default.nix @@ -101,6 +101,7 @@ in machine.copy_from_vm("/tmp/testerlog") machine.copy_from_vm("/tmp/hyprlog") machine.copy_from_vm("/tmp/exit_status") + machine.copy_from_vm("/tmp/exit_status_gtests") # Finally - shutdown machine.shutdown() From d40860e8e27c2abc50cebaae56e16b4ac10af667 Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:53:51 +0800 Subject: [PATCH 10/48] config/lua: cannot disable animation (#14215) * config/lua: cannot disable animation seems regression from 9ee5ff1f7181cc5fa575f1702802ccb78c21ba12 * typing typo (auto generated by cmake) --- meta/hl.meta.lua | 2 +- src/managers/animation/AnimationManager.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index 64f49b2ae..038c066b7 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -676,7 +676,7 @@ local __HL_Notification = {} local __HL_Timer = {} ---@class HL.Window ----@field acceptsInput boolean +---@field accepts_input boolean ---@field active boolean|nil ---@field address string ---@field at integer|table diff --git a/src/managers/animation/AnimationManager.cpp b/src/managers/animation/AnimationManager.cpp index c5c3ca542..76a124656 100644 --- a/src/managers/animation/AnimationManager.cpp +++ b/src/managers/animation/AnimationManager.cpp @@ -142,9 +142,9 @@ static void handleUpdate(CAnimatedVariable& av, bool warp) { const auto STEP = av.getCurveStep(); if constexpr (std::same_as) - updateColorVariable(av, STEP.value, STEP.finished); + updateColorVariable(av, STEP.value, STEP.finished || animationsDisabled); else - updateVariable(av, STEP.value, STEP.finished); + updateVariable(av, STEP.value, STEP.finished || animationsDisabled); av.onUpdate(); } From 8884b9e6ce608a22c993b1a871e864171626e8e5 Mon Sep 17 00:00:00 2001 From: MightyPlaza <123664421+MightyPlaza@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:59:03 +0000 Subject: [PATCH 11/48] binds: add an auto_consuming flag (#13919) --- meta/hl.meta.lua | 1 + src/config/legacy/ConfigManager.cpp | 33 ++++++++++++++++--- .../lua/bindings/LuaBindingsToplevel.cpp | 1 + src/config/lua/objects/LuaKeybind.cpp | 2 ++ src/debug/HyprCtl.cpp | 7 ++-- src/managers/KeybindManager.cpp | 6 ++-- src/managers/KeybindManager.hpp | 1 + 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index 038c066b7..96092e91e 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -580,6 +580,7 @@ local __HL_Group = {} ---@field set_enabled fun(self: HL.Keybind, ...): any ---@field unbind fun(self: HL.Keybind, ...): any ---@field arg string +---@field auto_consuming boolean ---@field catchall boolean ---@field click boolean ---@field description any diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 3e6fe6e99..2705c3959 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1478,6 +1478,7 @@ std::optional CConfigManager::handleBind(const std::string& command bool repeat = false; bool mouse = false; bool nonConsuming = false; + bool autoConsuming = false; bool transparent = false; bool ignoreMods = false; bool multiKey = false; @@ -1497,6 +1498,7 @@ std::optional CConfigManager::handleBind(const std::string& command case 'e': repeat = true; break; case 'm': mouse = true; break; case 'n': nonConsuming = true; break; + case 'a': autoConsuming = true; break; case 't': transparent = true; break; case 'i': ignoreMods = true; break; case 's': multiKey = true; break; @@ -1596,10 +1598,33 @@ std::optional CConfigManager::handleBind(const std::string& command return "Invalid catchall, catchall keybinds are only allowed in submaps."; } - g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, KEYSYMS, parsedKey.keycode, parsedKey.catchAll, MOD, MODS, HANDLER, - COMMAND, locked, m_currentSubmap, DESCRIPTION, release, repeat, longPress, - mouse, nonConsuming, transparent, ignoreMods, multiKey, hasDescription, dontInhibit, - click, drag, submapUniversal, deviceInclusive, devices}); + g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, + KEYSYMS, + parsedKey.keycode, + parsedKey.catchAll, + MOD, + MODS, + HANDLER, + COMMAND, + locked, + m_currentSubmap, + DESCRIPTION, + release, + repeat, + longPress, + mouse, + nonConsuming, + autoConsuming, + transparent, + ignoreMods, + multiKey, + hasDescription, + dontInhibit, + click, + drag, + submapUniversal, + deviceInclusive, + devices}); } return {}; diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index ec80c8999..8bdd1c193 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -166,6 +166,7 @@ static int hlBind(lua_State* L) { kb.locked = getBool("locked"); kb.release = getBool("release"); kb.nonConsuming = getBool("non_consuming"); + kb.autoConsuming = getBool("auto_consuming"); kb.transparent = getBool("transparent"); kb.ignoreMods = getBool("ignore_mods"); kb.dontInhibit = getBool("dont_inhibit"); diff --git a/src/config/lua/objects/LuaKeybind.cpp b/src/config/lua/objects/LuaKeybind.cpp index aab2735d1..ea2ce43d8 100644 --- a/src/config/lua/objects/LuaKeybind.cpp +++ b/src/config/lua/objects/LuaKeybind.cpp @@ -143,6 +143,8 @@ static int keybindIndex(lua_State* L) { lua_pushboolean(L, (*keybind)->release); else if (key == "non_consuming") lua_pushboolean(L, (*keybind)->nonConsuming); + else if (key == "auto_consuming") + lua_pushboolean(L, (*keybind)->autoConsuming); else if (key == "transparent") lua_pushboolean(L, (*keybind)->transparent); else if (key == "ignore_mods") diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index a3f5f9e00..b334a26e3 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1015,6 +1015,8 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request ret += "e"; if (kb->nonConsuming) ret += "n"; + if (kb->autoConsuming) + ret += "a"; if (kb->hasDescription) ret += "d"; @@ -1034,6 +1036,7 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request "repeat": {}, "longPress": {}, "non_consuming": {}, + "auto_consuming": {}, "has_description": {}, "modmask": {}, "submap": "{}", @@ -1046,8 +1049,8 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request "arg": "{}" }},)#", kb->locked ? "true" : "false", kb->mouse ? "true" : "false", kb->release ? "true" : "false", kb->repeat ? "true" : "false", kb->longPress ? "true" : "false", - kb->nonConsuming ? "true" : "false", kb->hasDescription ? "true" : "false", kb->modmask, escapeJSONStrings(kb->submap.name), kb->submapUniversal, - escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), + kb->nonConsuming ? "true" : "false", kb->autoConsuming ? "true" : "false", kb->hasDescription ? "true" : "false", kb->modmask, escapeJSONStrings(kb->submap.name), + kb->submapUniversal, escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), escapeJSONStrings(kb->arg)); } trimTrailingComma(ret); diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 8b1ec6533..4a5df4be7 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -653,7 +653,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP } if (pressed && k->release && !SPECIALDISPATCHER) { - if (k->nonConsuming) + if (k->nonConsuming || k->autoConsuming) continue; found = true; // suppress the event @@ -672,7 +672,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP continue; } else if (!k->release && !SPECIALDISPATCHER) { - if (k->nonConsuming) + if (k->nonConsuming || k->autoConsuming) continue; found = true; // suppress the event @@ -737,7 +737,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP m_repeatKeyTimer->updateTimeout(std::chrono::milliseconds(KEEB->m_repeatDelay)); } - if (!k->nonConsuming) + if (!k->nonConsuming && !(k->autoConsuming && !res.success)) found = true; } diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index fc51fdf0b..87cae14af 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -42,6 +42,7 @@ struct SKeybind { bool longPress = false; bool mouse = false; bool nonConsuming = false; + bool autoConsuming = false; bool transparent = false; bool ignoreMods = false; bool multiKey = false; From 859bbb11e16029bfa5ad6a1418bde23f29bd7d86 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:30:18 +0100 Subject: [PATCH 12/48] config/actions: fix misuse of ActionResult's error type (#14221) --- src/config/shared/ConfigErrors.hpp | 1 - src/config/shared/actions/ConfigActions.cpp | 46 ++++++++++----------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/config/shared/ConfigErrors.hpp b/src/config/shared/ConfigErrors.hpp index b67234007..b66510dcd 100644 --- a/src/config/shared/ConfigErrors.hpp +++ b/src/config/shared/ConfigErrors.hpp @@ -29,7 +29,6 @@ namespace Config { SConfigError() = default; SConfigError(std::string msg, eConfigErrorLevel lvl = eConfigErrorLevel::ERROR, eConfigErrorCode c = eConfigErrorCode::UNKNOWN) : message(std::move(msg)), level(lvl), code(c) {} - SConfigError(const char* msg) : message(msg ? msg : "") {} std::string message; eConfigErrorLevel level = eConfigErrorLevel::ERROR; diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index 1664df181..5db1aaf1e 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -235,7 +235,7 @@ ActionResult Actions::pinWindow(eTogglableAction action, std::optionalm_monitor.lock(); if (!PMONITOR) - return std::unexpected("Window has no monitor"); + return actionError("Window has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); window->layoutTarget()->assignToSpace(PMONITOR->m_activeWorkspace->m_space); window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_PINNED); @@ -291,7 +291,7 @@ ActionResult Actions::moveToWorkspace(PHLWORKSPACE ws, bool silent, std::optiona return {}; if (!ws) - return std::unexpected("Invalid workspace"); + return actionError("No workspace to move to", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_ARGUMENT); if (ws->m_id == window->workspaceID()) return {}; @@ -379,7 +379,7 @@ ActionResult Actions::moveFocus(Math::eDirection dir) { const auto PMONITOR = PLASTWINDOW->m_monitor.lock(); if (!PMONITOR) - return std::unexpected("Window has no monitor"); + return actionError("Window has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); if (dir == Math::DIRECTION_LEFT || dir == Math::DIRECTION_RIGHT) { if (STICKS(PLASTWINDOW->m_position.x, PMONITOR->m_position.x) && STICKS(PLASTWINDOW->m_size.x, PMONITOR->m_size.x)) @@ -422,7 +422,7 @@ ActionResult Actions::focus(PHLWINDOW window) { const auto PWORKSPACE = window->m_workspace; if (!PWORKSPACE) - return std::unexpected("Window has no workspace"); + return actionError("Window has no workspace", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); updateRelativeCursorCoords(); @@ -540,7 +540,7 @@ ActionResult Actions::moveCursorToCorner(int corner, std::optional w) return {}; if (corner < 0 || corner > 3) - return std::unexpected("Corner must be 0-3"); + return actionError("Corner must be 0 - 3", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); switch (corner) { case 0: g_pCompositor->warpCursorTo({window->m_realPosition->value().x, window->m_realPosition->value().y + window->m_realSize->value().y}, true); break; @@ -565,7 +565,7 @@ ActionResult Actions::resize(const Vector2D& size, bool relative, std::optional< return actionError("Window is fullscreen", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); if (!relative && (size.x < 1 || size.y < 1)) - return std::unexpected("Invalid size"); + return actionError("Invalid size", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); const auto delta = relative ? size : size - window->m_realSize->goal(); @@ -814,7 +814,7 @@ ActionResult Actions::setProp(const std::string& PROP, const std::string& VAL, s else if (PROP == "animation") parsePropTrivial(PWINDOW->m_ruleApplicator->animationStyle(), VAL); else - return std::unexpected("prop not found"); + return actionError("Invalid prop name", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); } catch (std::exception& e) { return std::unexpected(std::format("Error parsing prop value: {}", std::string(e.what()))); } @@ -886,14 +886,14 @@ ActionResult Actions::setGroupActive(int index, std::optional w) { ActionResult Actions::changeWorkspace(PHLWORKSPACE ws) { if (!ws) - return std::unexpected("Invalid workspace"); + return actionError("Invalid workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue("binds:hide_special_on_workspace_change"); static auto PWORKSPACECENTERON = CConfigValue("binds:workspace_center_on"); const auto PMONITOR = Desktop::focusState()->monitor(); if (!PMONITOR) - return std::unexpected("No monitor"); + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); if (ws->m_isSpecialWorkspace) { PMONITOR->setSpecialWorkspace(ws); @@ -907,7 +907,7 @@ ActionResult Actions::changeWorkspace(PHLWORKSPACE ws) { const auto PMONITORWORKSPACEOWNER = PMONITOR == ws->m_monitor ? PMONITOR : ws->m_monitor.lock(); if (!PMONITORWORKSPACEOWNER) - return std::unexpected("Workspace has no monitor"); + return actionError("Workspace has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); updateRelativeCursorCoords(); @@ -997,13 +997,13 @@ static PHLWORKSPACE resolveWorkspaceForChange(const std::string& args) { ActionResult Actions::changeWorkspace(const std::string& ws) { auto p = resolveWorkspaceForChange(ws); if (!p) - return std::unexpected("invalid workspace"); + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); return Actions::changeWorkspace(p); } ActionResult Actions::renameWorkspace(PHLWORKSPACE ws, const std::string& s) { if (!ws) - return std::unexpected("Invalid workspace"); + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); ws->rename(s); @@ -1012,9 +1012,9 @@ ActionResult Actions::renameWorkspace(PHLWORKSPACE ws, const std::string& s) { ActionResult Actions::moveToMonitor(PHLWORKSPACE ws, PHLMONITOR mon) { if (!ws) - return std::unexpected("Invalid workspace"); + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); if (!mon) - return std::unexpected("Invalid monitor"); + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); g_pCompositor->moveWorkspaceToMonitor(ws, mon); @@ -1023,16 +1023,16 @@ ActionResult Actions::moveToMonitor(PHLWORKSPACE ws, PHLMONITOR mon) { ActionResult Actions::changeWorkspaceOnCurrentMonitor(PHLWORKSPACE ws) { if (!ws) - return std::unexpected("Invalid workspace"); + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); const auto PCURRMONITOR = Desktop::focusState()->monitor(); if (!PCURRMONITOR) - return std::unexpected("No current monitor"); + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); if (ws->m_monitor != PCURRMONITOR) { const auto POLDMONITOR = ws->m_monitor.lock(); if (!POLDMONITOR) - return std::unexpected("Workspace has no monitor"); + return actionError("Workspace has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); if (POLDMONITOR->activeWorkspaceID() == ws->m_id) { g_pCompositor->swapActiveWorkspaces(POLDMONITOR, PCURRMONITOR); @@ -1047,11 +1047,11 @@ ActionResult Actions::changeWorkspaceOnCurrentMonitor(PHLWORKSPACE ws) { ActionResult Actions::toggleSpecial(PHLWORKSPACE special) { if (!special || !special->m_isSpecialWorkspace) - return std::unexpected("Invalid special workspace"); + return actionError("Bad special workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); const auto PMONITOR = Desktop::focusState()->monitor(); if (!PMONITOR) - return std::unexpected("No monitor"); + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); bool requestedWorkspaceIsAlreadyOpen = false; auto specialOpenOnMonitor = PMONITOR->activeSpecialWorkspaceID(); @@ -1090,7 +1090,7 @@ ActionResult Actions::toggleSpecial(PHLWORKSPACE special) { ActionResult Actions::focusMonitor(PHLMONITOR mon) { if (!mon) - return std::unexpected("Invalid monitor"); + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); tryMoveFocusToMonitor(mon); @@ -1099,7 +1099,7 @@ ActionResult Actions::focusMonitor(PHLMONITOR mon) { ActionResult Actions::swapActiveWorkspaces(PHLMONITOR mon1, PHLMONITOR mon2) { if (!mon1 || !mon2) - return std::unexpected("Invalid monitor"); + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); if (mon1 == mon2) return {}; @@ -1408,7 +1408,7 @@ ActionResult Actions::pass(std::optional w) { return {}; if (!g_pSeatManager->m_keyboard) - return std::unexpected("No keyboard"); + return actionError("No keyboard connected", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); const auto& S = *Config::Actions::state(); const auto XWTOXW = window->m_isX11 && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isX11; @@ -1476,7 +1476,7 @@ ActionResult Actions::pass(uint32_t modMask, uint32_t key, std::optionalm_keyboard) - return std::unexpected("No keyboard"); + return actionError("No keyboard connected", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); if (!isMouse) g_pSeatManager->setKeyboardFocus(window->wlSurface()->resource()); From bac49db9a11d5398963f4ad3265a577f8e94dc66 Mon Sep 17 00:00:00 2001 From: Zebra2711 <89755535+Zebra2711@users.noreply.github.com> Date: Fri, 1 May 2026 01:38:05 +0700 Subject: [PATCH 13/48] monitor: fix centered floating windows off-screen in special workspace (#14203) Signed-off-by: Zebra2711 --- hyprtester/src/tests/main/window.cpp | 22 ++++++++++++++++++++++ src/helpers/Monitor.cpp | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp index 4a4da0ccb..fab4ab767 100644 --- a/hyprtester/src/tests/main/window.cpp +++ b/hyprtester/src/tests/main/window.cpp @@ -588,6 +588,28 @@ TEST_CASE(issue14038) { // this should not crash hyprland. If we are alive, we good. } +TEST_CASE(specialFloatRecenters) { + if (!spawnKitty("kitty_special_float_recenter")) + FAIL_TEST("Could not spawn kitty"); + + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty_special_float_recenter' })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 10, y = 10, window = 'class:kitty_special_float_recenter' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'special:recenter', follow = false, window = 'class:kitty_special_float_recenter' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 50000, y = 50000, window = 'class:kitty_special_float_recenter' })")); + + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('recenter')")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_special_float_recenter' })")); + + const auto active = getFromSocket("/activewindow"); + EXPECT_CONTAINS(active, "class: kitty_special_float_recenter"); + EXPECT_CONTAINS(active, "size: 10,10"); + EXPECT_CONTAINS(active, "at: 955,535"); + + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('recenter')")); + Tests::killAllWindows(); + ASSERT(Tests::windowCount(), 0); +} + // TODO: decompose this into multiple test cases TEST_CASE(windows) { // test on workspace "window" diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index 657a8c11b..a5195a94f 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -1564,7 +1564,7 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) { if (VECNOTINRECT(MIDDLE, PMONFROMMIDDLE->m_position.x, PMONFROMMIDDLE->m_position.y, PMONFROMMIDDLE->m_position.x + PMONFROMMIDDLE->m_size.x, PMONFROMMIDDLE->m_position.y + PMONFROMMIDDLE->m_size.y)) { // not on any monitor, center - pos = middle() / 2.f - w->m_realSize->goal() / 2.f; + pos = middle() - w->m_realSize->goal() / 2.f; } else pos = pos - PMONFROMMIDDLE->m_position + m_position; From 546cc71565cd2ccd9277febb14dcebf513ace623 Mon Sep 17 00:00:00 2001 From: shezdy <77217897+shezdy@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:10:03 -0600 Subject: [PATCH 14/48] config/workspace-rules: support modifying persistent and monitor (#14217) * workspace rules: support modifying persistent and monitor * support modifying isDefault --- src/Compositor.cpp | 4 ++-- src/config/shared/workspace/WorkspaceRule.cpp | 10 +++++----- src/config/shared/workspace/WorkspaceRule.hpp | 4 ++-- src/config/shared/workspace/WorkspaceRuleManager.cpp | 2 +- src/debug/HyprCtl.cpp | 8 ++++---- src/desktop/Workspace.cpp | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 14a7843ec..c64300279 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -1699,7 +1699,7 @@ WORKSPACEID CCompositor::getNextAvailableNamedWorkspace() { // Give priority to persistent workspaces to avoid any conflicts between them. for (auto const& rule : Config::workspaceRuleMgr()->getAllWorkspaceRules()) { - if (!rule.m_isPersistent) + if (!rule.m_isPersistent.value_or(false)) continue; if (rule.m_workspaceId < -1 && rule.m_workspaceId < lowest) lowest = rule.m_workspaceId; @@ -3096,7 +3096,7 @@ void CCompositor::ensurePersistentWorkspacesPresent(const std::vector persistentFound; for (const auto& rule : rules) { - if (!rule.m_isPersistent) + if (!rule.m_isPersistent.value_or(false)) continue; PHLWORKSPACE PWORKSPACE = nullptr; diff --git a/src/config/shared/workspace/WorkspaceRule.cpp b/src/config/shared/workspace/WorkspaceRule.cpp index 28d5fcab7..dfd77d429 100644 --- a/src/config/shared/workspace/WorkspaceRule.cpp +++ b/src/config/shared/workspace/WorkspaceRule.cpp @@ -3,7 +3,7 @@ using namespace Config; void CWorkspaceRule::mergeLeft(const CWorkspaceRule& other) { - if (m_monitor.empty()) + if (!other.m_monitor.empty()) m_monitor = other.m_monitor; if (m_workspaceString.empty()) m_workspaceString = other.m_workspaceString; @@ -12,10 +12,10 @@ void CWorkspaceRule::mergeLeft(const CWorkspaceRule& other) { if (m_workspaceId == WORKSPACE_INVALID) m_workspaceId = other.m_workspaceId; - if (other.m_isDefault) - m_isDefault = true; - if (other.m_isPersistent) - m_isPersistent = true; + if (other.m_isDefault.has_value()) + m_isDefault = other.m_isDefault; + if (other.m_isPersistent.has_value()) + m_isPersistent = other.m_isPersistent; if (other.m_gapsIn.has_value()) m_gapsIn = other.m_gapsIn; if (other.m_gapsOut.has_value()) diff --git a/src/config/shared/workspace/WorkspaceRule.hpp b/src/config/shared/workspace/WorkspaceRule.hpp index 5d63e4349..e56a13663 100644 --- a/src/config/shared/workspace/WorkspaceRule.hpp +++ b/src/config/shared/workspace/WorkspaceRule.hpp @@ -27,8 +27,8 @@ namespace Config { std::string m_workspaceString = ""; std::string m_workspaceName = ""; WORKSPACEID m_workspaceId = -1; - bool m_isDefault = false; - bool m_isPersistent = false; + std::optional m_isDefault; + std::optional m_isPersistent; std::optional m_gapsIn; std::optional m_gapsOut; std::optional m_floatGaps = m_gapsOut; diff --git a/src/config/shared/workspace/WorkspaceRuleManager.cpp b/src/config/shared/workspace/WorkspaceRuleManager.cpp index 4c2c2b909..04b94a533 100644 --- a/src/config/shared/workspace/WorkspaceRuleManager.cpp +++ b/src/config/shared/workspace/WorkspaceRuleManager.cpp @@ -55,7 +55,7 @@ std::string CWorkspaceRuleManager::getDefaultWorkspaceFor(const std::string& nam if (!other->m_enabled) continue; - if (other->m_isDefault) { + if (other->m_isDefault.value_or(false)) { if (other->m_monitor == name) return other->m_workspaceString; if (other->m_monitor.starts_with("desc:")) { diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index b334a26e3..111714723 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -507,8 +507,8 @@ static std::string getWorkspaceRuleData(const Config::CWorkspaceRule& r, eHyprCt const auto boolToString = [](const bool b) -> std::string { return b ? "true" : "false"; }; if (format == eHyprCtlOutputFormat::FORMAT_JSON) { const std::string monitor = r.m_monitor.empty() ? "" : std::format(",\n \"monitor\": \"{}\"", escapeJSONStrings(r.m_monitor)); - const std::string default_ = sc(r.m_isDefault) ? std::format(",\n \"default\": {}", boolToString(r.m_isDefault)) : ""; - const std::string persistent = sc(r.m_isPersistent) ? std::format(",\n \"persistent\": {}", boolToString(r.m_isPersistent)) : ""; + const std::string default_ = sc(r.m_isDefault) ? std::format(",\n \"default\": {}", boolToString(r.m_isDefault.value())) : ""; + const std::string persistent = sc(r.m_isPersistent) ? std::format(",\n \"persistent\": {}", boolToString(r.m_isPersistent.value())) : ""; const std::string gapsIn = sc(r.m_gapsIn) ? std::format(",\n \"gapsIn\": [{}, {}, {}, {}]", r.m_gapsIn.value().m_top, r.m_gapsIn.value().m_right, r.m_gapsIn.value().m_bottom, r.m_gapsIn.value().m_left) : ""; @@ -531,8 +531,8 @@ static std::string getWorkspaceRuleData(const Config::CWorkspaceRule& r, eHyprCt return result; } else { const std::string monitor = std::format("\tmonitor: {}\n", r.m_monitor.empty() ? "" : escapeJSONStrings(r.m_monitor)); - const std::string default_ = std::format("\tdefault: {}\n", sc(r.m_isDefault) ? boolToString(r.m_isDefault) : ""); - const std::string persistent = std::format("\tpersistent: {}\n", sc(r.m_isPersistent) ? boolToString(r.m_isPersistent) : ""); + const std::string default_ = std::format("\tdefault: {}\n", sc(r.m_isDefault) ? boolToString(r.m_isDefault.value()) : ""); + const std::string persistent = std::format("\tpersistent: {}\n", sc(r.m_isPersistent) ? boolToString(r.m_isPersistent.value()) : ""); const std::string gapsIn = sc(r.m_gapsIn) ? std::format("\tgapsIn: {} {} {} {}\n", std::to_string(r.m_gapsIn.value().m_top), std::to_string(r.m_gapsIn.value().m_right), std::to_string(r.m_gapsIn.value().m_bottom), std::to_string(r.m_gapsIn.value().m_left)) : diff --git a/src/desktop/Workspace.cpp b/src/desktop/Workspace.cpp index 6d8e6cb49..60d8d9853 100644 --- a/src/desktop/Workspace.cpp +++ b/src/desktop/Workspace.cpp @@ -52,7 +52,7 @@ void CWorkspace::init(PHLWORKSPACE self) { m_inert = false; const auto WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(self).value_or(Config::CWorkspaceRule{}); - setPersistent(WORKSPACERULE.m_isPersistent); + setPersistent(WORKSPACERULE.m_isPersistent.value_or(false)); if (self->m_wasCreatedEmpty) if (auto cmd = WORKSPACERULE.m_onCreatedEmptyRunCmd) @@ -517,9 +517,9 @@ void CWorkspace::rename(const std::string& name) { m_name = name; const auto WORKSPACERULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(m_self.lock()).value_or(Config::CWorkspaceRule{}); - setPersistent(WORKSPACERULE.m_isPersistent); + setPersistent(WORKSPACERULE.m_isPersistent.value_or(false)); - if (WORKSPACERULE.m_isPersistent) + if (WORKSPACERULE.m_isPersistent.value_or(false)) g_pCompositor->ensurePersistentWorkspacesPresent(std::vector{WORKSPACERULE}, m_self.lock()); g_pEventManager->postEvent({.event = "renameworkspace", .data = std::to_string(m_id) + "," + m_name}); From cfb2deb6644cc2ce474bc8f5feb96d0768768441 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Thu, 30 Apr 2026 20:41:23 +0100 Subject: [PATCH 15/48] splashes: update splashes --- src/helpers/Splashes.hpp | 57 +++++++++++----------------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/src/helpers/Splashes.hpp b/src/helpers/Splashes.hpp index 4bc2814b9..64da47eea 100644 --- a/src/helpers/Splashes.hpp +++ b/src/helpers/Splashes.hpp @@ -58,49 +58,22 @@ namespace NSplashes { "Thanks ThatOneCalculator!", "The AUR packages always work, except for the times they don't.", "Funny animation compositor woo", - "3 years!", + "4 years!", "Beauty will save the world", // 4th ricing comp winner - zacoons' choice - // music reference / quote section - "J'remue le ciel, le jour, la nuit.", - "aezakmi, aezakmi, aezakmi, aezakmi, aezakmi, aezakmi, aezakmi!", - "Wir sind schon sehr lang zusammen...", - "I see a red door and I want it painted black.", - "Take on me, take me on...", - "You spin me right round baby right round", - "Stayin' alive, stayin' alive", - "Say no way, say no way ya, no way!", - "Ground control to Major Tom...", - "Alors on danse", - "And all that I can see, is just a yellow lemon tree.", - "Got a one-way ticket to the blues", - "Is this the real life, is this just fantasy", - "What's in your head, in your head?", - "We're all living in America, America, America.", - "I'm still standing, better than I ever did", - "Here comes the sun, bringing you love and shining on everyone", - "Two trailer park girls go round the outside", - "With the lights out, it's less dangerous", - "Here we go back, this is the moment, tonight is the night", - "Now you're just somebody that I used to know...", - "Black bird, black moon, black sky", - "Some legends are told, some turn to dust or to gold", - "Your brain gets smart, but your head gets dumb.", - "Save your mercy for someone who needs it more", - "You're gonna hear my voice when I shout it out loud", - "Ding ding pch n daa, bam-ba-ba-re-bam baram bom bom baba-bam-bam-bommm", - "Súbeme la radio que esta es mi canción", - "I'm beggin', beggin' you", - "Never gonna let you down (I am trying!)", - "Hier kommt die Sonne", - "Kickstart my heart, give it a start", - "Fear of the dark, I have a constant fear that something's always near", - "Komm mit, reih dich ein.", - "I wish I had an angel for one moment of love", - "We're the children of the dark", - "You float like a feather, in a beautiful world", - "Demons come at night and they bring the end", - "All I wanna say is that they don't really care about us", - "Has he lost his mind? Can he see or is he blind?", + "Configration", + "RIP hyprlang", + "better call vaxry", + "i hypr therefore i land", + "five. hundred. config errors.", + "bundled with anime girls for your convenience", + "now with 200% more hypr and land", + "daily dose of rice", + "Removed Herobrine", + "You should try quickshell!", + "He was an X11 boy, she was a Wayland girl...", + "How do I exit vim????", + "Now with lua, it might as well be awesome.", + "Have you ran your daily fastfetch yet?" // clang-format on }; From 450d8a0bf703f3ec460d367f38d61b5966daf8fe Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:41:55 +0100 Subject: [PATCH 16/48] xdg-shell: queue state updates for toplevel (#14227) ref https://github.com/hyprwm/Hyprland/discussions/14224 --- src/managers/eventLoop/EventLoopManager.cpp | 38 ++++++++++++-- src/managers/eventLoop/EventLoopManager.hpp | 19 +++++-- src/protocols/XDGShell.cpp | 58 +++++++++++++-------- src/protocols/XDGShell.hpp | 16 +++--- 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/managers/eventLoop/EventLoopManager.cpp b/src/managers/eventLoop/EventLoopManager.cpp index 03a8d7c92..6c661a6fc 100644 --- a/src/managers/eventLoop/EventLoopManager.cpp +++ b/src/managers/eventLoop/EventLoopManager.cpp @@ -16,6 +16,17 @@ using namespace Hyprutils::OS; #define TIMESPEC_NSEC_PER_SEC 1000000000L +static uint64_t LAST_DO_LATER_SEQ = 1; + +SEventLoopDoLaterLock::SEventLoopDoLaterLock(uint64_t seq_) : seq(seq_) { + ; +} + +SEventLoopDoLaterLock::~SEventLoopDoLaterLock() { + if (g_pEventLoopManager && seq > 0) + g_pEventLoopManager->removeDoLater(seq); +} + CEventLoopManager::CEventLoopManager(wl_display* display, wl_event_loop* wlEventLoop) { m_timers.timerfd = CFileDescriptor{timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC)}; m_wayland.loop = wlEventLoop; @@ -201,11 +212,13 @@ void CEventLoopManager::nudgeTimers() { timerfd_settime(m_timers.timerfd.get(), TFD_TIMER_ABSTIME, &ts, nullptr); } -void CEventLoopManager::doLater(const std::function& fn) { - m_idle.fns.emplace_back(fn); +uint64_t CEventLoopManager::doLater(const std::function& fn) { + const uint64_t NEW_SEQ = ++LAST_DO_LATER_SEQ; + + m_idle.fns.emplace_back(std::make_pair<>(NEW_SEQ, fn)); if (m_idle.eventSource) - return; + return NEW_SEQ; m_idle.eventSource = wl_event_loop_add_idle( m_wayland.loop, @@ -215,11 +228,26 @@ void CEventLoopManager::doLater(const std::function& fn) { IDLE->fns.clear(); IDLE->eventSource = nullptr; for (auto& f : fns) { - if (f) - f(); + if (f.second) + f.second(); } }, &m_idle); + + return NEW_SEQ; +} + +void CEventLoopManager::removeDoLater(uint64_t seq) { + std::erase_if(m_idle.fns, [&seq](const auto& e) { return e.first == seq; }); + + if (m_idle.fns.empty() && m_idle.eventSource) { + wl_event_source_remove(m_idle.eventSource); + m_idle.eventSource = nullptr; + } +} + +UP CEventLoopManager::doLaterLock(const std::function& fn) { + return makeUnique(doLater(fn)); } void CEventLoopManager::doOnReadable(CFileDescriptor fd, std::function&& fn) { diff --git a/src/managers/eventLoop/EventLoopManager.hpp b/src/managers/eventLoop/EventLoopManager.hpp index 7999dc595..646734555 100644 --- a/src/managers/eventLoop/EventLoopManager.hpp +++ b/src/managers/eventLoop/EventLoopManager.hpp @@ -14,6 +14,13 @@ namespace Aquamarine { struct SPollFD; }; +struct SEventLoopDoLaterLock { + SEventLoopDoLaterLock(uint64_t seq); + ~SEventLoopDoLaterLock(); + + uint64_t seq = 0; +}; + class CEventLoopManager { public: CEventLoopManager(wl_display* display, wl_event_loop* wlEventLoop); @@ -30,12 +37,16 @@ class CEventLoopManager { // schedules a recalc of the timers void scheduleRecalc(); - // schedules a function to run later, aka in a wayland idle event. - void doLater(const std::function& fn); + // schedules a function to run later, aka in a wayland idle event. Returns a sequence which can be used to remove it. + uint64_t doLater(const std::function& fn); + void removeDoLater(uint64_t seq); + + // automatically cleaned up doLater instance + [[nodiscard]] UP doLaterLock(const std::function& fn); struct SIdleData { - wl_event_source* eventSource = nullptr; - std::vector> fns; + wl_event_source* eventSource = nullptr; + std::vector>> fns; }; struct SReadableWaiter { diff --git a/src/protocols/XDGShell.cpp b/src/protocols/XDGShell.cpp index 774346d4b..3c7029503 100644 --- a/src/protocols/XDGShell.cpp +++ b/src/protocols/XDGShell.cpp @@ -4,6 +4,7 @@ #include "../Compositor.hpp" #include "../managers/SeatManager.hpp" #include "../managers/ANRManager.hpp" +#include "../managers/eventLoop/EventLoopManager.hpp" #include "../helpers/Monitor.hpp" #include "core/Seat.hpp" #include "core/Compositor.hpp" @@ -286,7 +287,7 @@ bool CXDGToplevelResource::anyChildModal() { uint32_t CXDGToplevelResource::setSize(const Vector2D& size) { m_pendingApply.size = size; - applyState(); + scheduleStateApplication(); return m_owner->scheduleConfigure(); } @@ -300,7 +301,9 @@ uint32_t CXDGToplevelResource::setMaximized(bool maximized) { m_pendingApply.states.push_back(XDG_TOPLEVEL_STATE_MAXIMIZED); else if (!maximized && set) std::erase(m_pendingApply.states, XDG_TOPLEVEL_STATE_MAXIMIZED); - applyState(); + + scheduleStateApplication(); + return m_owner->scheduleConfigure(); } @@ -314,7 +317,9 @@ uint32_t CXDGToplevelResource::setFullscreen(bool fullscreen) { m_pendingApply.states.push_back(XDG_TOPLEVEL_STATE_FULLSCREEN); else if (!fullscreen && set) std::erase(m_pendingApply.states, XDG_TOPLEVEL_STATE_FULLSCREEN); - applyState(); + + scheduleStateApplication(); + return m_owner->scheduleConfigure(); } @@ -328,7 +333,9 @@ uint32_t CXDGToplevelResource::setActive(bool active) { m_pendingApply.states.push_back(XDG_TOPLEVEL_STATE_ACTIVATED); else if (!active && set) std::erase(m_pendingApply.states, XDG_TOPLEVEL_STATE_ACTIVATED); - applyState(); + + scheduleStateApplication(); + return m_owner->scheduleConfigure(); } @@ -345,22 +352,32 @@ uint32_t CXDGToplevelResource::setSuspeneded(bool sus) { m_pendingApply.states.push_back(XDG_TOPLEVEL_STATE_SUSPENDED); else if (!sus && set) std::erase(m_pendingApply.states, XDG_TOPLEVEL_STATE_SUSPENDED); - applyState(); + + scheduleStateApplication(); + return m_owner->scheduleConfigure(); } -void CXDGToplevelResource::applyState() { - wl_array arr; - wl_array_init(&arr); +void CXDGToplevelResource::scheduleStateApplication() { - if (!m_pendingApply.states.empty()) { - wl_array_add(&arr, m_pendingApply.states.size() * sizeof(int)); - memcpy(arr.data, m_pendingApply.states.data(), m_pendingApply.states.size() * sizeof(int)); - } + if (m_stateUpdate) + return; - m_resource->sendConfigure(m_pendingApply.size.x, m_pendingApply.size.y, &arr); + m_stateUpdate = g_pEventLoopManager->doLaterLock([this] { + wl_array arr; + wl_array_init(&arr); - wl_array_release(&arr); + if (!m_pendingApply.states.empty()) { + wl_array_add(&arr, m_pendingApply.states.size() * sizeof(int)); + memcpy(arr.data, m_pendingApply.states.data(), m_pendingApply.states.size() * sizeof(int)); + } + + m_resource->sendConfigure(m_pendingApply.size.x, m_pendingApply.size.y, &arr); + + wl_array_release(&arr); + + m_stateUpdate.reset(); + }); } void CXDGToplevelResource::close() { @@ -516,8 +533,6 @@ CXDGSurfaceResource::CXDGSurfaceResource(SP resource_, SPresetRole(); } @@ -531,22 +546,19 @@ SP CXDGSurfaceResource::fromResource(wl_resource* res) { return data ? data->m_self.lock() : nullptr; } -static void onConfigure(void* data) { - sc(data)->configure(); -} - uint32_t CXDGSurfaceResource::scheduleConfigure() { - if (m_configureSource) + if (m_stateUpdate) return m_scheduledSerial; - m_configureSource = wl_event_loop_add_idle(g_pCompositor->m_wlEventLoop, onConfigure, this); + m_stateUpdate = g_pEventLoopManager->doLaterLock([this] { configure(); }); + m_scheduledSerial = wl_display_next_serial(g_pCompositor->m_wlDisplay); return m_scheduledSerial; } void CXDGSurfaceResource::configure() { - m_configureSource = nullptr; + m_stateUpdate.reset(); m_resource->sendConfigure(m_scheduledSerial); } diff --git a/src/protocols/XDGShell.hpp b/src/protocols/XDGShell.hpp index 3d94d2829..79f154326 100644 --- a/src/protocols/XDGShell.hpp +++ b/src/protocols/XDGShell.hpp @@ -18,6 +18,7 @@ class CXDGPopupResource; class CSeatGrab; class CWLSurfaceResource; class CXDGDialogV1Resource; +struct SEventLoopDoLaterLock; struct SXDGPositionerState { Vector2D requestedSize; @@ -149,8 +150,11 @@ class CXDGToplevelResource { std::vector> m_children; private: - SP m_resource; - void applyState(); + SP m_resource; + + UP m_stateUpdate; + + void scheduleStateApplication(); }; class CXDGSurfaceRole : public ISurfaceRole { @@ -202,12 +206,12 @@ class CXDGSurfaceResource { void configure(); private: - SP m_resource; + SP m_resource; - uint32_t m_lastConfigureSerial = 0; - uint32_t m_scheduledSerial = 0; + UP m_stateUpdate; - wl_event_source* m_configureSource = nullptr; + uint32_t m_lastConfigureSerial = 0; + uint32_t m_scheduledSerial = 0; // std::vector> m_popups; From c6fc1cab99ff8a3d721ae0e5937512e8d6f58a71 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:31:29 +0100 Subject: [PATCH 17/48] helpers/systemInfo: extract info fns (#14222) --- src/Compositor.cpp | 30 ++-- src/debug/HyprCtl.cpp | 230 ++---------------------------- src/helpers/MiscFunctions.cpp | 48 ------- src/helpers/MiscFunctions.hpp | 1 - src/helpers/SystemInfo.cpp | 257 ++++++++++++++++++++++++++++++++++ src/helpers/SystemInfo.hpp | 11 ++ src/main.cpp | 35 ++++- 7 files changed, 323 insertions(+), 289 deletions(-) create mode 100644 src/helpers/SystemInfo.cpp create mode 100644 src/helpers/SystemInfo.hpp diff --git a/src/Compositor.cpp b/src/Compositor.cpp index c64300279..ab371df22 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -10,6 +10,7 @@ #include "desktop/history/WorkspaceHistoryTracker.hpp" #include "desktop/view/Group.hpp" #include "helpers/Splashes.hpp" +#include "helpers/SystemInfo.hpp" #include "config/ConfigValue.hpp" #include "config/legacy/ConfigManager.hpp" #include "config/shared/inotify/ConfigWatcher.hpp" @@ -232,26 +233,7 @@ CCompositor::CCompositor(bool onlyConfig) : m_onlyConfigVerification(onlyConfig) Log::logger->initIS(m_instancePath); - Log::logger->log(Log::DEBUG, "Instance Signature: {}", m_instanceSignature); - - Log::logger->log(Log::DEBUG, "Runtime directory: {}", m_instancePath); - - Log::logger->log(Log::DEBUG, "Hyprland PID: {}", m_hyprlandPID); - - Log::logger->log(Log::DEBUG, "===== SYSTEM INFO: ====="); - - logSystemInfo(); - - Log::logger->log(Log::DEBUG, "========================"); - - Log::logger->log(Log::DEBUG, "\n\n"); // pad - - Log::logger->log(Log::INFO, "If you are crashing, or encounter any bugs, please consult https://wiki.hypr.land/Crashes-and-Bugs/\n\n"); - setRandomSplash(); - - Log::logger->log(Log::DEBUG, "\nCurrent splash: {}\n\n", m_currentSplash); - bumpNofile(); } @@ -365,6 +347,16 @@ void CCompositor::initServer(std::string socketName, int socketFd) { m_initialized = true; + Log::logger->log(Log::DEBUG, "Instance Signature: {}", m_instanceSignature); + Log::logger->log(Log::DEBUG, "Runtime directory: {}", m_instancePath); + Log::logger->log(Log::DEBUG, "Hyprland PID: {}", m_hyprlandPID); + Log::logger->log(Log::DEBUG, "===== SYSTEM INFO: ====="); + Log::logger->log(Log::DEBUG, "{}", Helpers::SystemInfo::getSystemInfo()); + Log::logger->log(Log::DEBUG, "========================"); + Log::logger->log(Log::DEBUG, "\n\n"); // pad + Log::logger->log(Log::INFO, "If you are crashing, or encounter any bugs, please consult https://wiki.hypr.land/Crashes-and-Bugs/\n\n"); + Log::logger->log(Log::DEBUG, "\nCurrent splash: {}\n\n", m_currentSplash); + m_drm.fd = m_aqBackend->drmFD(); Log::logger->log(Log::DEBUG, "Running on DRMFD: {}", m_drm.fd); diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index 111714723..278e92c6a 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -49,9 +49,10 @@ using namespace Hyprutils::OS; #include "../devices/ITouch.hpp" #include "../devices/Tablet.hpp" #include "../protocols/GlobalShortcuts.hpp" -#include "debug/log/RollingLogFollow.hpp" -#include "config/ConfigManager.hpp" -#include "helpers/MiscFunctions.hpp" +#include "../debug/log/RollingLogFollow.hpp" +#include "../config/ConfigManager.hpp" +#include "../helpers/MiscFunctions.hpp" +#include "../helpers/SystemInfo.hpp" #include "../desktop/view/LayerSurface.hpp" #include "../desktop/view/Group.hpp" #include "../desktop/rule/Engine.hpp" @@ -1061,193 +1062,19 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request } std::string versionRequest(eHyprCtlOutputFormat format, std::string request) { + return Helpers::SystemInfo::getVersion(format); +} - auto commitMsg = trim(GIT_COMMIT_MESSAGE); - std::ranges::replace(commitMsg, '#', ' '); - - if (format == eHyprCtlOutputFormat::FORMAT_NORMAL) { - std::string result = std::format("Hyprland {} built from branch {} at commit {} {} ({}).\n" - "Date: {}\n" - "Tag: {}, commits: {}\n", - HYPRLAND_VERSION, GIT_BRANCH, GIT_COMMIT_HASH, GIT_DIRTY, commitMsg, GIT_COMMIT_DATE, GIT_TAG, GIT_COMMITS); - - result += "\n"; - result += getBuiltSystemLibraryNames(); - result += "\n"; - result += "Version ABI string: "; - result += __hyprland_api_get_hash(); - result += "\n"; - -#if (!ISDEBUG && !defined(NO_XWAYLAND) && !defined(BUILT_WITH_NIX)) - result += "no flags were set\n"; -#else - result += "flags set:\n"; -#if ISDEBUG - result += "debug\n"; -#endif -#ifdef NO_XWAYLAND - result += "no xwayland\n"; -#endif -#ifdef BUILT_WITH_NIX - result += "nix\n"; -#endif -#endif - return result; - } else { - std::string result = std::format( - R"#({{ - "branch": "{}", - "commit": "{}", - "version": "{}", - "dirty": {}, - "commit_message": "{}", - "commit_date": "{}", - "tag": "{}", - "commits": "{}", - "buildAquamarine": "{}", - "buildHyprlang": "{}", - "buildHyprutils": "{}", - "buildHyprcursor": "{}", - "buildHyprgraphics": "{}", - "systemAquamarine": "{}", - "systemHyprlang": "{}", - "systemHyprutils": "{}", - "systemHyprcursor": "{}", - "systemHyprgraphics": "{}", - "abiHash": "{}", - "flags": [)#", - GIT_BRANCH, GIT_COMMIT_HASH, HYPRLAND_VERSION, (strcmp(GIT_DIRTY, "dirty") == 0 ? "true" : "false"), escapeJSONStrings(commitMsg), GIT_COMMIT_DATE, GIT_TAG, - GIT_COMMITS, AQUAMARINE_VERSION, HYPRLANG_VERSION, HYPRUTILS_VERSION, HYPRCURSOR_VERSION, HYPRGRAPHICS_VERSION, getSystemLibraryVersion("aquamarine"), - getSystemLibraryVersion("hyprlang"), getSystemLibraryVersion("hyprutils"), getSystemLibraryVersion("hyprcursor"), getSystemLibraryVersion("hyprgraphics"), - __hyprland_api_get_hash()); - -#if ISDEBUG - result += "\"debug\","; -#endif -#ifdef NO_XWAYLAND - result += "\"no xwayland\","; -#endif -#ifdef BUILT_WITH_NIX - result += "\"nix\","; -#endif - - trimTrailingComma(result); - - result += "]\n}"; - - return result; - } - - return ""; // make the compiler happy +static std::string statusRequest(eHyprCtlOutputFormat format, std::string request) { + return Helpers::SystemInfo::getStatus(format); } std::string systemInfoRequest(eHyprCtlOutputFormat format, std::string request) { - std::string result = versionRequest(eHyprCtlOutputFormat::FORMAT_NORMAL, ""); - static auto check = [](bool y) -> std::string { return y ? "✔️" : "❌"; }; - static auto backend = [](Aquamarine::eBackendType t) -> std::string { - switch (t) { - case Aquamarine::AQ_BACKEND_DRM: return "drm"; - case Aquamarine::AQ_BACKEND_HEADLESS: return "headless"; - case Aquamarine::AQ_BACKEND_WAYLAND: return "wayland"; - default: break; - } - return "?"; - }; - - result += "\n\nSystem Information:\n"; - - struct utsname unameInfo; - - uname(&unameInfo); - - result += "System name: " + std::string{unameInfo.sysname} + "\n"; - result += "Node name: " + std::string{unameInfo.nodename} + "\n"; - result += "Release: " + std::string{unameInfo.release} + "\n"; - result += "Version: " + std::string{unameInfo.version} + "\n"; - result += "\n"; - result += getBuiltSystemLibraryNames(); - result += "\n"; - - result += "\n\n"; - -#if defined(__DragonFly__) || defined(__FreeBSD__) - const std::string GPUINFO = execAndGet("pciconf -lv | grep -F -A4 vga"); -#elif defined(__arm__) || defined(__aarch64__) - std::string GPUINFO; - const std::filesystem::path dev_tree = "/proc/device-tree"; - try { - if (std::filesystem::exists(dev_tree) && std::filesystem::is_directory(dev_tree)) { - std::for_each(std::filesystem::directory_iterator(dev_tree), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& entry) { - if (std::filesystem::is_directory(entry) && entry.path().filename().string().starts_with("soc")) { - std::for_each(std::filesystem::directory_iterator(entry.path()), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& sub_entry) { - if (std::filesystem::is_directory(sub_entry) && sub_entry.path().filename().string().starts_with("gpu")) { - std::filesystem::path file_path = sub_entry.path() / "compatible"; - std::ifstream file(file_path); - if (file) - GPUINFO.append(std::istreambuf_iterator(file), std::istreambuf_iterator()); - } - }); - } - }); - } - } catch (...) { GPUINFO = "error"; } -#else - const std::string GPUINFO = execAndGet("lspci -vnn | grep -E '(VGA|Display|3D)'"); -#endif - result += "GPU information: \n" + GPUINFO; - if (GPUINFO.contains("NVIDIA") && std::filesystem::exists("/proc/driver/nvidia/version")) { - std::ifstream file("/proc/driver/nvidia/version"); - std::string line; - if (file.is_open()) { - while (std::getline(file, line)) { - if (!line.contains("NVRM")) - continue; - result += line; - result += "\n"; - } - } else - result += "error"; - } - result += "\n\n"; - - if (std::ifstream file("/etc/os-release"); file.is_open()) { - std::stringstream buffer; - buffer << file.rdbuf(); - result += "os-release: " + buffer.str() + "\n\n"; - } else - result += "os-release: error\n\n"; - - result += "plugins:\n"; - if (g_pPluginSystem) { - for (auto const& pl : g_pPluginSystem->getAllPlugins()) { - result += std::format(" {} by {} ver {}\n", pl->m_name, pl->m_author, pl->m_version); - } - } else - result += "\tunknown: not runtime\n"; - - if (g_pHyprOpenGL) { - result += std::format("\nExplicit sync: {}", g_pHyprOpenGL->m_exts.EGL_ANDROID_native_fence_sync_ext ? "supported" : "missing"); - result += std::format("\nGL ver: {}", g_pHyprOpenGL->m_eglContextVersion == CHyprOpenGLImpl::EGL_CONTEXT_GLES_3_2 ? "3.2" : "3.0"); - } - - if (g_pCompositor) { - result += std::format("\nBackend: {}", g_pCompositor->m_aqBackend->hasSession() ? "drm" : "sessionless"); - - result += "\n\nMonitor info:"; - - for (const auto& m : g_pCompositor->m_monitors) { - result += std::format("\n\tPanel {}: {}x{}, {} {} {} {} -> backend {}\n\t\texplicit {}\n\t\tedid:\n\t\t\thdr {}\n\t\t\tchroma {}\n\t\t\tbt2020 {}\n\t\tvrr capable " - "{}\n\t\tnon-desktop {}\n\t\t", - m->m_name, sc(m->m_pixelSize.x), sc(m->m_pixelSize.y), m->m_output->name, m->m_output->make, m->m_output->model, m->m_output->serial, - backend(m->m_output->getBackend()->type()), check(m->m_output->supportsExplicit), check(m->m_output->parsedEDID.hdrMetadata.has_value()), - check(m->m_output->parsedEDID.chromaticityCoords.has_value()), check(m->m_output->parsedEDID.supportsBT2020), check(m->m_output->vrrCapable), - check(m->m_output->nonDesktop)); - } - } + auto result = Helpers::SystemInfo::getSystemInfo(); if (g_pHyprCtl && g_pHyprCtl->m_currentRequestParams.sysInfoConfig) { - result += "\n======Config-Start======\n"; + result += "\n\n======Config-Start======\n"; result += Config::mgr()->getConfigString(); result += "\n======Config-End========\n"; } @@ -2097,43 +1924,6 @@ static std::string submapRequest(eHyprCtlOutputFormat format, std::string reques return format == FORMAT_JSON ? std::format("\"{}\"\n", escapeJSONStrings(submap)) : (submap + "\n"); } -static std::string statusRequest(eHyprCtlOutputFormat format, std::string request) { - Aquamarine::eBackendType backendType = Aquamarine::eBackendType::AQ_BACKEND_NULL; - - for (const auto& i : g_pCompositor->m_aqBackend->getImplementations()) { - if (i->type() == Aquamarine::eBackendType::AQ_BACKEND_NULL || i->type() == Aquamarine::eBackendType::AQ_BACKEND_HEADLESS) - continue; - - backendType = i->type(); - break; - } - - std::string backendStr; - - switch (backendType) { - case Aquamarine::AQ_BACKEND_DRM: backendStr = "drm"; break; - case Aquamarine::AQ_BACKEND_WAYLAND: backendStr = "wayland"; break; - default: backendStr = "error"; break; - } - - if (format == eHyprCtlOutputFormat::FORMAT_JSON) { - - return std::format(R"#( -{{ - "configProvider": "{}", - "backend": "{}" -}} -)#", - Config::typeToString(Config::mgr()->type()), backendStr); - } - - return std::format(R"#( -configProvider: {} -backend: {} -)#", - Config::typeToString(Config::mgr()->type()), backendStr); -} - static std::string reloadShaders(eHyprCtlOutputFormat format, std::string request) { CVarList vars(request, 0, ' '); diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index 5e37f19fb..5aa58a2c8 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -523,54 +523,6 @@ std::string execAndGet(const char* cmd) { return proc.stdOut(); } -void logSystemInfo() { - struct utsname unameInfo; - - uname(&unameInfo); - - Log::logger->log(Log::DEBUG, "System name: {}", std::string{unameInfo.sysname}); - Log::logger->log(Log::DEBUG, "Node name: {}", std::string{unameInfo.nodename}); - Log::logger->log(Log::DEBUG, "Release: {}", std::string{unameInfo.release}); - Log::logger->log(Log::DEBUG, "Version: {}", std::string{unameInfo.version}); - - Log::logger->log(Log::DEBUG, "\n"); - -#if defined(__DragonFly__) || defined(__FreeBSD__) - const std::string GPUINFO = execAndGet("pciconf -lv | grep -F -A4 vga"); -#elif defined(__arm__) || defined(__aarch64__) - std::string GPUINFO; - const std::filesystem::path dev_tree = "/proc/device-tree"; - try { - if (std::filesystem::exists(dev_tree) && std::filesystem::is_directory(dev_tree)) { - std::for_each(std::filesystem::directory_iterator(dev_tree), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& entry) { - if (std::filesystem::is_directory(entry) && entry.path().filename().string().starts_with("soc")) { - std::for_each(std::filesystem::directory_iterator(entry.path()), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& sub_entry) { - if (std::filesystem::is_directory(sub_entry) && sub_entry.path().filename().string().starts_with("gpu")) { - std::filesystem::path file_path = sub_entry.path() / "compatible"; - std::ifstream file(file_path); - if (file) - GPUINFO.append(std::istreambuf_iterator(file), std::istreambuf_iterator()); - } - }); - } - }); - } - } catch (...) { GPUINFO = "error"; } -#else - const std::string GPUINFO = execAndGet("lspci -vnn | grep -E '(VGA|Display|3D)'"); -#endif - Log::logger->log(Log::DEBUG, "GPU information:\n{}\n", GPUINFO); - - if (GPUINFO.contains("NVIDIA")) { - Log::logger->log(Log::WARN, "Warning: you're using an NVIDIA GPU. Make sure you follow the instructions on the wiki if anything is amiss.\n"); - } - - // log etc - Log::logger->log(Log::DEBUG, "os-release:"); - - Log::logger->log(Log::DEBUG, "{}", NFsUtils::readFileAsString("/etc/os-release").value_or("error")); -} - int64_t getPPIDof(int64_t pid) { #if defined(KERN_PROC_PID) int mib[] = { diff --git a/src/helpers/MiscFunctions.hpp b/src/helpers/MiscFunctions.hpp index 26c5c7adc..0528be58c 100644 --- a/src/helpers/MiscFunctions.hpp +++ b/src/helpers/MiscFunctions.hpp @@ -27,7 +27,6 @@ bool isDirection(const char&); SWorkspaceIDName getWorkspaceIDNameFromString(const std::string&); std::optional cleanCmdForWorkspace(const std::string&, std::string); float vecToRectDistanceSquared(const Vector2D& vec, const Vector2D& p1, const Vector2D& p2); -void logSystemInfo(); std::string execAndGet(const char*); int64_t getPPIDof(int64_t pid); std::expected configStringToInt(const std::string&); diff --git a/src/helpers/SystemInfo.cpp b/src/helpers/SystemInfo.cpp new file mode 100644 index 000000000..3ebdd16da --- /dev/null +++ b/src/helpers/SystemInfo.cpp @@ -0,0 +1,257 @@ +#include "SystemInfo.hpp" + +#include "../Compositor.hpp" +#include "../version.h" +#include "../plugins/PluginAPI.hpp" +#include "../plugins/PluginSystem.hpp" +#include "../render/OpenGL.hpp" +#include "../config/ConfigManager.hpp" + +#include + +#include +#include +#include +#include +#include + +using namespace Helpers::SystemInfo; +using namespace Helpers; +using namespace Hyprutils::String; +using namespace Render::GL; + +static void trimTrailingComma(std::string& str) { + if (!str.empty() && str.back() == ',') + str.pop_back(); +} + +std::string SystemInfo::getStatus(eHyprCtlOutputFormat fmt) { + Aquamarine::eBackendType backendType = Aquamarine::eBackendType::AQ_BACKEND_NULL; + + for (const auto& i : g_pCompositor->m_aqBackend->getImplementations()) { + if (i->type() == Aquamarine::eBackendType::AQ_BACKEND_NULL || i->type() == Aquamarine::eBackendType::AQ_BACKEND_HEADLESS) + continue; + + backendType = i->type(); + break; + } + + std::string backendStr; + + switch (backendType) { + case Aquamarine::AQ_BACKEND_DRM: backendStr = "drm"; break; + case Aquamarine::AQ_BACKEND_WAYLAND: backendStr = "wayland"; break; + default: backendStr = "error"; break; + } + + if (fmt == eHyprCtlOutputFormat::FORMAT_JSON) { + + return std::format(R"#( +{{ + "configProvider": "{}", + "backend": "{}" +}} +)#", + Config::typeToString(Config::mgr()->type()), backendStr); + } + + return std::format(R"#( +configProvider: {} +backend: {} +)#", + Config::typeToString(Config::mgr()->type()), backendStr); +} + +std::string SystemInfo::getVersion(eHyprCtlOutputFormat fmt) { + + auto commitMsg = trim(GIT_COMMIT_MESSAGE); + std::ranges::replace(commitMsg, '#', ' '); + + if (fmt == eHyprCtlOutputFormat::FORMAT_NORMAL) { + std::string result = std::format("Hyprland {} built from branch {} at commit {} {} ({}).\n" + "Date: {}\n" + "Tag: {}, commits: {}\n", + HYPRLAND_VERSION, GIT_BRANCH, GIT_COMMIT_HASH, GIT_DIRTY, commitMsg, GIT_COMMIT_DATE, GIT_TAG, GIT_COMMITS); + + result += "\n"; + result += getBuiltSystemLibraryNames(); + result += "\n"; + result += "Version ABI string: "; + result += __hyprland_api_get_hash(); + result += "\n"; + +#if (!ISDEBUG && !defined(NO_XWAYLAND) && !defined(BUILT_WITH_NIX)) + result += "no flags were set\n"; +#else + result += "flags set:\n"; +#if ISDEBUG + result += "debug\n"; +#endif +#ifdef NO_XWAYLAND + result += "no xwayland\n"; +#endif +#ifdef BUILT_WITH_NIX + result += "nix\n"; +#endif +#endif + return result; + } else { + std::string result = std::format( + R"#({{ + "branch": "{}", + "commit": "{}", + "version": "{}", + "dirty": {}, + "commit_message": "{}", + "commit_date": "{}", + "tag": "{}", + "commits": "{}", + "buildAquamarine": "{}", + "buildHyprlang": "{}", + "buildHyprutils": "{}", + "buildHyprcursor": "{}", + "buildHyprgraphics": "{}", + "systemAquamarine": "{}", + "systemHyprlang": "{}", + "systemHyprutils": "{}", + "systemHyprcursor": "{}", + "systemHyprgraphics": "{}", + "abiHash": "{}", + "flags": [)#", + GIT_BRANCH, GIT_COMMIT_HASH, HYPRLAND_VERSION, (GIT_DIRTY == std::string_view{"dirty"} ? "true" : "false"), escapeJSONStrings(commitMsg), GIT_COMMIT_DATE, GIT_TAG, + GIT_COMMITS, AQUAMARINE_VERSION, HYPRLANG_VERSION, HYPRUTILS_VERSION, HYPRCURSOR_VERSION, HYPRGRAPHICS_VERSION, getSystemLibraryVersion("aquamarine"), + getSystemLibraryVersion("hyprlang"), getSystemLibraryVersion("hyprutils"), getSystemLibraryVersion("hyprcursor"), getSystemLibraryVersion("hyprgraphics"), + __hyprland_api_get_hash()); + +#if ISDEBUG + result += "\"debug\","; +#endif +#ifdef NO_XWAYLAND + result += "\"no xwayland\","; +#endif +#ifdef BUILT_WITH_NIX + result += "\"nix\","; +#endif + + trimTrailingComma(result); + + result += "]\n}"; + + return result; + } + + return ""; // make the compiler happy +} + +std::string SystemInfo::getSystemInfo() { + std::string result = getVersion(eHyprCtlOutputFormat::FORMAT_NORMAL); + + static auto check = [](bool y) -> std::string { return y ? "✔️" : "❌"; }; + static auto backend = [](Aquamarine::eBackendType t) -> std::string { + switch (t) { + case Aquamarine::AQ_BACKEND_DRM: return "drm"; + case Aquamarine::AQ_BACKEND_HEADLESS: return "headless"; + case Aquamarine::AQ_BACKEND_WAYLAND: return "wayland"; + default: break; + } + return "?"; + }; + + result += "\n\nSystem Information:\n"; + + struct utsname unameInfo; + + uname(&unameInfo); + + result += "System name: " + std::string{unameInfo.sysname} + "\n"; + result += "Node name: " + std::string{unameInfo.nodename} + "\n"; + result += "Release: " + std::string{unameInfo.release} + "\n"; + result += "Version: " + std::string{unameInfo.version} + "\n"; + result += "\n"; + result += getBuiltSystemLibraryNames(); + result += "\n"; + + result += "\n\n"; + +#if defined(__DragonFly__) || defined(__FreeBSD__) + const std::string GPUINFO = execAndGet("pciconf -lv | grep -F -A4 vga"); +#elif defined(__arm__) || defined(__aarch64__) + std::string GPUINFO; + const std::filesystem::path dev_tree = "/proc/device-tree"; + try { + if (std::filesystem::exists(dev_tree) && std::filesystem::is_directory(dev_tree)) { + std::for_each(std::filesystem::directory_iterator(dev_tree), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& entry) { + if (std::filesystem::is_directory(entry) && entry.path().filename().string().starts_with("soc")) { + std::for_each(std::filesystem::directory_iterator(entry.path()), std::filesystem::directory_iterator{}, [&](const std::filesystem::directory_entry& sub_entry) { + if (std::filesystem::is_directory(sub_entry) && sub_entry.path().filename().string().starts_with("gpu")) { + std::filesystem::path file_path = sub_entry.path() / "compatible"; + std::ifstream file(file_path); + if (file) + GPUINFO.append(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + }); + } + }); + } + } catch (...) { GPUINFO = "error"; } +#else + const std::string GPUINFO = execAndGet("lspci -vnn | grep -E '(VGA|Display|3D)'"); +#endif + result += "GPU information: \n" + GPUINFO; + if (GPUINFO.contains("NVIDIA") && std::filesystem::exists("/proc/driver/nvidia/version")) { + std::ifstream file("/proc/driver/nvidia/version"); + std::string line; + if (file.is_open()) { + while (std::getline(file, line)) { + if (!line.contains("NVRM")) + continue; + result += line; + result += "\n"; + } + } else + result += "error"; + } + result += "\n\n"; + + if (std::ifstream file("/etc/os-release"); file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + result += "os-release: " + buffer.str() + "\n\n"; + } else + result += "os-release: error\n\n"; + + result += "plugins:\n"; + if (g_pPluginSystem) { + for (auto const& pl : g_pPluginSystem->getAllPlugins()) { + result += std::format(" {} by {} ver {}\n", pl->m_name, pl->m_author, pl->m_version); + } + } else + result += "\tunknown: not runtime\n"; + + if (g_pHyprOpenGL) { + result += std::format("\nExplicit sync: {}", g_pHyprOpenGL->m_exts.EGL_ANDROID_native_fence_sync_ext ? "supported" : "missing"); + result += std::format("\nGL ver: {}", g_pHyprOpenGL->m_eglContextVersion == CHyprOpenGLImpl::EGL_CONTEXT_GLES_3_2 ? "3.2" : "3.0"); + } + + if (g_pCompositor) { + result += std::format("\nBackend: {}", g_pCompositor->m_aqBackend->hasSession() ? "drm" : "sessionless"); + + result += "\n\nMonitor info:"; + + for (const auto& m : g_pCompositor->m_monitors) { + result += std::format("\n\tPanel {}: {}x{}, {} {} {} {} -> backend {}\n\t\texplicit {}\n\t\tedid:\n\t\t\thdr {}\n\t\t\tchroma {}\n\t\t\tbt2020 {}\n\t\tvrr capable " + "{}\n\t\tnon-desktop {}\n\t\t", + m->m_name, sc(m->m_pixelSize.x), sc(m->m_pixelSize.y), m->m_output->name, m->m_output->make, m->m_output->model, m->m_output->serial, + backend(m->m_output->getBackend()->type()), check(m->m_output->supportsExplicit), check(m->m_output->parsedEDID.hdrMetadata.has_value()), + check(m->m_output->parsedEDID.chromaticityCoords.has_value()), check(m->m_output->parsedEDID.supportsBT2020), check(m->m_output->vrrCapable), + check(m->m_output->nonDesktop)); + } + } + + result += "\n\nState:\n"; + result += getStatus(FORMAT_NORMAL); + + result += "\n\n"; + + return result; +} \ No newline at end of file diff --git a/src/helpers/SystemInfo.hpp b/src/helpers/SystemInfo.hpp new file mode 100644 index 000000000..5eec45c24 --- /dev/null +++ b/src/helpers/SystemInfo.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "../SharedDefs.hpp" + +namespace Helpers::SystemInfo { + std::string getSystemInfo(); + std::string getVersion(eHyprCtlOutputFormat fmt); + std::string getStatus(eHyprCtlOutputFormat fmt); +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index faf780832..b85146f60 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -202,8 +202,41 @@ int main(int argc, char** argv) { return 1; } - if (!verifyConfig) + if (!verifyConfig) { std::println("Welcome to Hyprland!"); + std::println(R"#( + + YY UJ + YYY UUJ + XXXY UUUU + zXXXX UUUUU + zzzzX UUUUJ + cczzz UUUUJ + vccccz UUUUUJ + vvcccc UUUUUJ + vvvvv UUUUJ + uuuvv UUUUJ + uuuuu UUUUU + nnnuu UUUUU + nnnnn YUUUU + xxnn YUUU + xxxn YYUU + xxxx YYUU + rxxx YYYY + rrrx YYYY + rrrx XXXY + rrrr XXXX + rrrr zzXX + rrrr zzzz + rrrrr ccczz + rrrrrx vccccc + rrrrxxxx uuvvvvvc + rrxxxxxxnnnnuuuuuv + xxxxxnnnnu + + +)#"); + } // let's init the compositor. // it initializes basic Wayland stuff in the constructor. From a45de66d22d696cd4e60444176641d59bc65db6c Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Fri, 1 May 2026 03:18:14 +0530 Subject: [PATCH 18/48] cleanup: avoid repeated weak_ptr::lock() usage in MasterAlgorithm (#14226) --- .../algorithm/tiled/master/MasterAlgorithm.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index 46506391c..a6849d2ac 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -476,10 +476,12 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { return stateErr("no master node"); const auto NEWCHILD = PMASTER->pTarget.lock(); + if (!NEWCHILD) + return stateErr("master target expired"); const bool IGNORE_IF_MASTER = vars.size() >= 2 && std::ranges::any_of(vars, [](const auto& e) { return e == "ignoremaster"; }); - if (PMASTER->pTarget.lock() != PWINDOW->layoutTarget()) { + if (NEWCHILD != PWINDOW->layoutTarget()) { const auto& NEWMASTER = PWINDOW->layoutTarget(); const bool newFocusToChild = vars.size() >= 2 && vars[1] == "child"; g_layoutManager->switchTargets(NEWMASTER, NEWCHILD); @@ -516,8 +518,12 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { const auto& ARG = vars[1]; // returns empty string if out of bounds - if (PMASTER->pTarget.lock() != PWINDOW->layoutTarget()) { - switchToWindow(PMASTER->pTarget.lock()); + const auto TARGET = PMASTER->pTarget.lock(); + if (!TARGET) + return stateErr("master target expired"); + + if (TARGET != PWINDOW->layoutTarget()) { + switchToWindow(TARGET); // save previously focused window (only for `previous` mode) if (ARG == "previous") m_workspaceData.focusMasterPrev = PWINDOW->layoutTarget(); From e2ba19535ddf3a3f4d0ce3236403895d9e7661cc Mon Sep 17 00:00:00 2001 From: Maxim Andreev Date: Fri, 1 May 2026 15:39:16 +0400 Subject: [PATCH 19/48] tests: fix ConfigLuaValueTypes - boolBadType test, 0 and 1 are allowed integer values for bool type (#14240) --- tests/config/lua/ConfigValueTypes.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config/lua/ConfigValueTypes.cpp b/tests/config/lua/ConfigValueTypes.cpp index 76de98bf9..4ee818761 100644 --- a/tests/config/lua/ConfigValueTypes.cpp +++ b/tests/config/lua/ConfigValueTypes.cpp @@ -89,7 +89,7 @@ TEST(ConfigLuaValueTypes, boolBadType) { CLuaConfigBool value(false); - lua_pushinteger(L, 1); + lua_pushinteger(L, 2); const auto err = value.parse(L); lua_pop(L, 1); From 241eeae51a8570fa48cffc89492b666697c4715f Mon Sep 17 00:00:00 2001 From: Vaxry Date: Fri, 1 May 2026 12:50:51 +0100 Subject: [PATCH 20/48] config/lua: add noop --- meta/hl.meta.lua | 1 + src/config/lua/bindings/LuaBindingsDispatchers.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index 96092e91e..e0fd096ae 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -793,6 +793,7 @@ local __HL_API = {} ---@field force_renderer_reload fun(...): any ---@field global fun(...): any ---@field layout fun(...): any +---@field no_op fun(...): any ---@field pass fun(...): any ---@field send_key_state fun(...): any ---@field send_shortcut fun(...): any diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index fc84895ac..0b6ca07a0 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1093,6 +1093,15 @@ static int hlFocus(lua_State* L) { return Internal::configError(L, "hl.focus: unrecognized arguments. Expected one of: direction, monitor, window, urgent_or_last, last"); } +static int dsp_noop(lua_State* L) { + return 0; +} + +static int hlNoop(lua_State* L) { + lua_pushcclosure(L, dsp_noop, 0); + return 1; +} + static int dsp_toggleSpecial(lua_State* L) { std::string name = lua_isnil(L, lua_upvalueindex(1)) ? "" : lua_tostring(L, lua_upvalueindex(1)); const auto& [workspaceID, workspaceName, isAutoID] = getWorkspaceIDNameFromString("special:" + name); @@ -1266,6 +1275,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { Internal::setFn(L, "force_renderer_reload", hlForceRendererReload); Internal::setFn(L, "force_idle", hlForceIdle); Internal::setFn(L, "focus", hlFocus); + Internal::setFn(L, "no_op", hlNoop); } lua_setfield(L, -2, "dsp"); From 5f9df52b5593bda5bd60786691f2bac61f3ae89d Mon Sep 17 00:00:00 2001 From: Vaxry Date: Fri, 1 May 2026 15:40:11 +0100 Subject: [PATCH 21/48] protocolMgr: set m_self properly when updating mirrored outputs When we have onMonitorModeChange, we can't leave an output with a null m_self as that will fuck up other things wanting a ref ref https://github.com/basecamp/omarchy/issues/5457 --- src/managers/ProtocolManager.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp index 3be4523d9..d77d1b0a6 100644 --- a/src/managers/ProtocolManager.cpp +++ b/src/managers/ProtocolManager.cpp @@ -97,7 +97,9 @@ void CProtocolManager::onMonitorModeChange(PHLMONITOR pMonitor) { else if (!ISMIRROR && (!PROTO::outputs.contains(pMonitor->m_name) || PROTO::outputs.at(pMonitor->m_name)->isDefunct())) { if (PROTO::outputs.contains(pMonitor->m_name)) PROTO::outputs.erase(pMonitor->m_name); - PROTO::outputs.emplace(pMonitor->m_name, makeShared(&wl_output_interface, 4, std::format("WLOutput ({})", pMonitor->m_name), pMonitor->m_self.lock())); + auto p = PROTO::outputs.emplace(pMonitor->m_name, + makeShared(&wl_output_interface, 4, std::format("WLOutput ({})", pMonitor->m_name), pMonitor->m_self.lock())); + p.first->second->m_self = p.first->second; } if (PROTO::colorManagement && g_pCompositor->shouldChangePreferredImageDescription()) { From 4ab3a40398d1331b69e033ab14533524c554a002 Mon Sep 17 00:00:00 2001 From: Eren <51189118+astridlyre@users.noreply.github.com> Date: Fri, 1 May 2026 15:50:02 -0700 Subject: [PATCH 22/48] screenshare: adjust session cleanup and event emission order (#14229) * fix(screenshare): adjust session cleanup and event emission order Revised the handling of `stoppedListener` initialization in `getManagedSession` to ensure correct scoping and lifecycle management. Updated the `stop` method in `CScreenshareSession` to adjust the order of `screenshareEvents` and `stopped.emit()` to prevent potential use-after-free scenarios. * fix(screenshare): ensure managedSession removal uses consistent target reference This change updates the lambda in the stopped listener to use a pre-fetched target pointer for comparison when erasing sessions. * fix(screenshare): use early-return and smart ptr comparison in session cleanup --- .../screenshare/ScreenshareManager.cpp | 18 ++++++++++-------- .../screenshare/ScreenshareSession.cpp | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/managers/screenshare/ScreenshareManager.cpp b/src/managers/screenshare/ScreenshareManager.cpp index 6a0f5b958..6b5c0aded 100644 --- a/src/managers/screenshare/ScreenshareManager.cpp +++ b/src/managers/screenshare/ScreenshareManager.cpp @@ -140,16 +140,18 @@ WP CScreenshareManager::getManagedSession(eScreenshareType m_sessions.emplace_back(session); it = m_managedSessions.emplace(m_managedSessions.end(), makeUnique(std::move(session))); + + auto& managed = *it; + managed->stoppedListener = managed->m_session->m_events.stopped.listen([managed = WP(managed)]() { + if (!managed) + return; + + const auto& session = managed->m_session; + std::erase_if(Screenshare::mgr()->m_managedSessions, [&session](const auto& s) { return s && s->m_session == session; }); + }); } - auto& session = *it; - - session->stoppedListener = session->m_session->m_events.stopped.listen([session = WP(session)]() { - if (!session.expired()) - std::erase_if(Screenshare::mgr()->m_managedSessions, [&](const auto& s) { return s && s->m_session.get() == session->m_session.get(); }); - }); - - return session->m_session; + return (*it)->m_session; } bool CScreenshareManager::isOutputBeingSSd(PHLMONITOR monitor) { diff --git a/src/managers/screenshare/ScreenshareSession.cpp b/src/managers/screenshare/ScreenshareSession.cpp index e3676ec0b..1f7aeb881 100644 --- a/src/managers/screenshare/ScreenshareSession.cpp +++ b/src/managers/screenshare/ScreenshareSession.cpp @@ -56,9 +56,9 @@ void CScreenshareSession::stop() { if (m_stopped) return; m_stopped = true; - m_events.stopped.emit(); screenshareEvents(false); + m_events.stopped.emit(); } bool CScreenshareSession::isActive() { From 6e1fcfa81e55a7d78e40fdb5c41700c18b0e1268 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Fri, 1 May 2026 23:50:26 +0100 Subject: [PATCH 23/48] hyprctl: fix getoption with custom types (#14243) --- src/debug/HyprCtl.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index 278e92c6a..e84a55527 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1644,6 +1644,14 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re return std::format("str: {}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(void*)) return std::format("custom type: {}\nset: {}", rc((*rc(VAL))->getData())->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::IComplexConfigValue)) + return std::format("custom type: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CCssGapData)) + return std::format("css gap data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CGradientValueData)) + return std::format("gradient data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CFontWeightConfigValueData)) + return std::format("font weight data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); } else { if (TYPE == typeid(Config::INTEGER)) return std::format(R"({{"option": "{}", "int": {}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); @@ -1662,6 +1670,15 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re else if (TYPE == typeid(void*)) return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem, rc((*rc(VAL))->getData())->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::IComplexConfigValue)) + return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CCssGapData)) + return std::format(R"({{"option": "{}", "css": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CGradientValueData)) + return std::format(R"({{"option": "{}", "gradient": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CFontWeightConfigValueData)) + return std::format(R"({{"option": "{}", "font_weight": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), + VAR.setByUser); } return "invalid type (internal error)"; From c7b8fe13c10d6a4392bfda9bcfc041e0ea0b7c00 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Fri, 1 May 2026 23:50:57 +0100 Subject: [PATCH 24/48] tests: fix gtests crashing (#14244) --- src/config/lua/ConfigManager.cpp | 2 +- src/config/lua/LuaEventHandler.cpp | 9 ++++++--- tests/config/lua/LuaObjectsBasic.cpp | 14 ++++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/config/lua/ConfigManager.cpp b/src/config/lua/ConfigManager.cpp index 7819ff2e8..b4fc791d5 100644 --- a/src/config/lua/ConfigManager.cpp +++ b/src/config/lua/ConfigManager.cpp @@ -160,7 +160,7 @@ static int safeLuaRequire(lua_State* L) { WP Lua::mgr() { auto& mgr = Config::mgr(); - if (mgr->type() != CONFIG_LUA) + if (!mgr || mgr->type() != CONFIG_LUA) return nullptr; return dynamicPointerCast(WP(mgr)); diff --git a/src/config/lua/LuaEventHandler.cpp b/src/config/lua/LuaEventHandler.cpp index eb4f6bc52..fbb44dac9 100644 --- a/src/config/lua/LuaEventHandler.cpp +++ b/src/config/lua/LuaEventHandler.cpp @@ -61,15 +61,18 @@ void CLuaEventHandler::dispatch(const std::string& name, int nargs, const std::f lua_rawgeti(m_lua, LUA_REGISTRYINDEX, sub->second.luaRef); pushArgs(); - int status = LUA_OK; - if (auto* mgr = CConfigManager::fromLuaState(m_lua); mgr) + auto* mgr = CConfigManager::fromLuaState(m_lua); + + int status = LUA_OK; + if (mgr) status = mgr->guardedPCall(nargs, 0, 0, CConfigManager::LUA_TIMEOUT_EVENT_CALLBACK_MS, std::format("hl.on(\"{}\") callback", name)); else status = lua_pcall(m_lua, nargs, 0, 0); if (status != LUA_OK) { const char* err = lua_tostring(m_lua, -1); - Config::Lua::mgr()->addError(std::format("hl.on(\"{}\") callback: {}", name, err ? err : "(unknown)")); + if (mgr) + mgr->addError(std::format("hl.on(\"{}\") callback: {}", name, err ? err : "(unknown)")); lua_pop(m_lua, 1); } } diff --git a/tests/config/lua/LuaObjectsBasic.cpp b/tests/config/lua/LuaObjectsBasic.cpp index 10758b7d4..3f09aa1e9 100644 --- a/tests/config/lua/LuaObjectsBasic.cpp +++ b/tests/config/lua/LuaObjectsBasic.cpp @@ -48,6 +48,13 @@ namespace { lua_pop(L, 1); return v; } + + bool isGlobalNil(lua_State* L, const char* name) { + lua_getglobal(L, name); + const bool isnil = lua_isnil(L, -1); + lua_pop(L, 1); + return isnil; + } } TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) { @@ -138,10 +145,9 @@ TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) { Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); - EXPECT_NE(luaL_dostring(L, "kb.foo = 1"), LUA_OK); - ASSERT_TRUE(lua_isstring(L, -1)); - EXPECT_NE(std::string(lua_tostring(L, -1)).find("read-only"), std::string::npos); - lua_pop(L, 1); + luaL_dostring(L, "kb.foo = 1"); + luaL_dostring(L, "x = kb.foo"); + EXPECT_TRUE(isGlobalNil(L, "x")); } TEST(ConfigLuaObjects, keybindSupportsEqAndToString) { From f9723133cc909874999e6181bddf286033070e0c Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 1 May 2026 19:02:01 -0400 Subject: [PATCH 25/48] gestures: add live pinch cursor zoom (#14049) --- hyprtester/plugin/src/main.cpp | 122 ++++++++++++++++++ hyprtester/src/tests/main/gestures.cpp | 38 ++++++ hyprtester/src/tests/main/groups.cpp | 2 + hyprtester/test.lua | 1 + src/helpers/MonitorZoomController.cpp | 21 ++- src/helpers/MonitorZoomController.hpp | 15 ++- .../trackpad/gestures/CursorZoomGesture.cpp | 57 +++++++- .../trackpad/gestures/CursorZoomGesture.hpp | 5 + 8 files changed, 249 insertions(+), 12 deletions(-) diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index c7d6f4fdf..3bb6e2b23 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -2,11 +2,13 @@ #include #include #include +#include #define private public #include #include #include +#include #include #include #include @@ -17,6 +19,7 @@ #undef private #include +#include #include using namespace Hyprutils::Utils; using namespace Hyprutils::String; @@ -156,6 +159,96 @@ static SDispatchResult simulateGesture(std::string in) { return {.success = true}; } +static SDispatchResult pinchUpdate(std::string in) { + CVarList data(in); + uint32_t fingers = 2; + double scale = 1.0; + Vector2D delta = {}; + double rotation{}; + + if (data.size() < 2) + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[0]); n) + fingers = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[1]); n) + scale = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (data.size() > 2) { + if (const auto n = strToNumber(data[2]); n) + delta.x = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + if (data.size() > 3) { + if (const auto n = strToNumber(data[3]); n) + delta.y = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + if (data.size() > 4) { + if (const auto n = strToNumber(data[4]); n) + rotation = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + g_pTrackpadGestures->gestureUpdate(IPointer::SPinchUpdateEvent{ + .fingers = fingers, + .delta = delta, + .scale = scale, + .rotation = rotation, + }); + + return {}; +} + +static SDispatchResult pinchEnd(std::string in) { + g_pTrackpadGestures->gestureEnd(IPointer::SPinchEndEvent{}); + + return {}; +} + +static SDispatchResult expectCursorZoom(std::string in) { + CVarList data(in); + float expected = 1.F; + float delta = 0.01F; + + if (data.size() < 1) + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[0]); n) + expected = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (data.size() > 1) { + if (const auto n = strToNumber(data[1]); n) + delta = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + const auto PMONITOR = g_pCompositor->getMonitorFromVector(g_pInputManager->getMouseCoordsInternal()); + + if (!PMONITOR) + return {.success = false, .error = "No monitor under cursor"}; + + const auto actual = PMONITOR->m_cursorZoom->value(); + + if (std::abs(actual - expected) > delta) + return {.success = false, .error = std::format("Expected cursor zoom {} ± {}, got {}", expected, delta, actual)}; + + return {}; +} + static SDispatchResult vkb(std::string in) { auto tkb0 = CTestKeyboard::create(false); auto tkb1 = CTestKeyboard::create(false); @@ -375,6 +468,32 @@ static int luaGesture(lua_State* L) { return luaResult(L, ::simulateGesture(std::format("{},{}", direction, fingers))); } +static int luaPinchUpdate(lua_State* L) { + std::string in = std::format("{},{}", (int)luaL_checkinteger(L, 1), (double)luaL_checknumber(L, 2)); + + if (lua_gettop(L) > 2) + in += std::format(",{}", (double)luaL_checknumber(L, 3)); + if (lua_gettop(L) > 3) + in += std::format(",{}", (double)luaL_checknumber(L, 4)); + if (lua_gettop(L) > 4) + in += std::format(",{}", (double)luaL_checknumber(L, 5)); + + return luaResult(L, ::pinchUpdate(in)); +} + +static int luaPinchEnd(lua_State* L) { + return luaResult(L, ::pinchEnd("")); +} + +static int luaExpectCursorZoom(lua_State* L) { + const auto expected = (double)luaL_checknumber(L, 1); + + if (lua_gettop(L) > 1) + return luaResult(L, ::expectCursorZoom(std::format("{},{}", expected, (double)luaL_checknumber(L, 2)))); + + return luaResult(L, ::expectCursorZoom(std::format("{}", expected))); +} + static int luaScroll(lua_State* L) { return luaResult(L, ::scroll(std::to_string((double)luaL_checknumber(L, 1)))); } @@ -425,6 +544,9 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { addLuaFn("vkb", ::luaVkb); addLuaFn("alt", ::luaAlt); addLuaFn("gesture", ::luaGesture); + addLuaFn("pinch_update", ::luaPinchUpdate); + addLuaFn("pinch_end", ::luaPinchEnd); + addLuaFn("expect_cursor_zoom", ::luaExpectCursorZoom); addLuaFn("scroll", ::luaScroll); addLuaFn("click", ::luaClick); addLuaFn("keybind", ::luaKeybind); diff --git a/hyprtester/src/tests/main/gestures.cpp b/hyprtester/src/tests/main/gestures.cpp index f83247123..f82a1613b 100644 --- a/hyprtester/src/tests/main/gestures.cpp +++ b/hyprtester/src/tests/main/gestures.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include "../shared.hpp" using namespace Hyprutils::OS; using namespace Hyprutils::Memory; +using namespace Hyprutils::String; #define UP CUniquePointer #define SP CSharedPointer @@ -178,4 +180,40 @@ TEST_CASE(gestures) { OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); } + const std::string cursorPosBeforePinch = getFromSocket("/cursorpos"); + + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 500, y = 500 })")); + OK(getFromSocket("/eval hl.config({ cursor = { zoom_factor = 1 } })")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 1.2)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.2, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 1.6)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.6, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_end()")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.6, 0.01)")); + + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 0.64)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_end()")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + + const auto comma = cursorPosBeforePinch.find(','); + + if (comma != std::string::npos) { + auto xSv = std::string_view(cursorPosBeforePinch).substr(0, comma); + auto ySv = std::string_view(cursorPosBeforePinch).substr(comma + 1); + while (!xSv.empty() && xSv.front() == ' ') + xSv.remove_prefix(1); + while (!ySv.empty() && ySv.front() == ' ') + ySv.remove_prefix(1); + + const auto x = strToNumber(xSv); + const auto y = strToNumber(ySv); + + if (!x || !y) + FAIL_TEST("Failed to restore cursor pos"); + + OK(getFromSocket(std::format("/dispatch hl.dsp.cursor.move({{ x = {}, y = {} }})", x.value(), y.value()))); + } } diff --git a/hyprtester/src/tests/main/groups.cpp b/hyprtester/src/tests/main/groups.cpp index 7e1c63b6b..e1d4dd9be 100644 --- a/hyprtester/src/tests/main/groups.cpp +++ b/hyprtester/src/tests/main/groups.cpp @@ -192,6 +192,7 @@ TEST_CASE(groups) { NLog::log("{}Disable autogrouping", Colors::YELLOW); OK(getFromSocket("/eval hl.config({ group = { auto_group = false } })")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); NLog::log("{}Spawn kittyProcC", Colors::YELLOW); auto kittyProcC = Tests::spawnKitty(); @@ -206,6 +207,7 @@ TEST_CASE(groups) { EXPECT_COUNT_STRING(str, "at: 22,22", 2); } + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 0 } })")); OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })")); OK(getFromSocket("/dispatch hl.dsp.group.active({ index = 1 })")); OK(getFromSocket("/eval hl.config({ group = { auto_group = true } })")); diff --git a/hyprtester/test.lua b/hyprtester/test.lua index 6f39b34a3..d1f8657e8 100644 --- a/hyprtester/test.lua +++ b/hyprtester/test.lua @@ -290,5 +290,6 @@ hl.gesture({ fingers = 5, direction = "left", action = function() hl.dispatch(hl hl.gesture({ fingers = 5, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "t", window = "activewindow" })) end }) hl.gesture({ fingers = 4, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "return", window = "activewindow" })) end }) hl.gesture({ fingers = 4, direction = "left", action = function() hl.dispatch(hl.dsp.cursor.move_to_corner({ corner = 1, window = "activewindow" })) end }) +hl.gesture({ fingers = 2, direction = "pinch", action = "cursorZoom", zoom_level = "1", mode = "live" }) hl.gesture({ fingers = 2, direction = "right", action = "float", disable_inhibit = true }) diff --git a/src/helpers/MonitorZoomController.cpp b/src/helpers/MonitorZoomController.cpp index c1b6cc6af..b59e1eb22 100644 --- a/src/helpers/MonitorZoomController.cpp +++ b/src/helpers/MonitorZoomController.cpp @@ -7,11 +7,27 @@ #include "desktop/DesktopTypes.hpp" #include "render/Renderer.hpp" +void CMonitorZoomController::pinAnchor(const Vector2D& anchor) { + m_pinnedAnchor = anchor; + m_anchorPinned = true; +} + +void CMonitorZoomController::clearAnchor() { + m_anchorPinned = false; +} + +Vector2D CMonitorZoomController::getAnchor(const PHLMONITORREF& monitor) { + if (m_anchorPinned) + return m_pinnedAnchor; + + return g_pInputManager->getMouseCoordsInternal() - monitor->m_position; +} + void CMonitorZoomController::zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData) { const auto m = m_renderData.pMonitor; auto monbox = CBox(0, 0, m->m_size.x, m->m_size.y); const auto ZOOM = g_pHyprRenderer->m_renderData.mouseZoomFactor; - const auto MOUSE = g_pInputManager->getMouseCoordsInternal() - m->m_position; + const auto MOUSE = getAnchor(m); if (m_lastZoomLevel != ZOOM) { if (m_resetCameraState) { @@ -83,8 +99,7 @@ void CMonitorZoomController::applyZoomTransform(CBox& monbox, const Render::SRen if (*PZOOMDETACHEDCAMERA && !INITANIM) zoomWithDetachedCamera(monbox, m_renderData); else { - const auto ZOOMCENTER = - g_pHyprRenderer->m_renderData.mouseZoomUseMouse ? (g_pInputManager->getMouseCoordsInternal() - m->m_position) * m->m_scale : m->m_transformedSize / 2.f; + const auto ZOOMCENTER = g_pHyprRenderer->m_renderData.mouseZoomUseMouse ? getAnchor(m) * m->m_scale : m->m_transformedSize / 2.f; monbox.translate(-ZOOMCENTER).scale(ZOOM).translate(*PZOOMRIGID ? m->m_transformedSize / 2.0 : ZOOMCENTER); } diff --git a/src/helpers/MonitorZoomController.hpp b/src/helpers/MonitorZoomController.hpp index 94373bafc..ce3ad5546 100644 --- a/src/helpers/MonitorZoomController.hpp +++ b/src/helpers/MonitorZoomController.hpp @@ -1,6 +1,7 @@ #pragma once #include "./math/Math.hpp" +#include "../desktop/DesktopTypes.hpp" namespace Render { struct SRenderData; @@ -10,12 +11,18 @@ class CMonitorZoomController { public: bool m_resetCameraState = true; + void pinAnchor(const Vector2D& anchor); + void clearAnchor(); + void applyZoomTransform(CBox& monbox, const Render::SRenderData& m_renderData); private: - void zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData); + void zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData); + Vector2D getAnchor(const PHLMONITORREF& monitor); - CBox m_camera; - float m_lastZoomLevel = 1.0f; - bool m_padCamEdges = true; + CBox m_camera; + Vector2D m_pinnedAnchor = {}; + float m_lastZoomLevel = 1.0f; + bool m_padCamEdges = true; + bool m_anchorPinned = false; }; diff --git a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp index 515edbb6b..418b8f7a2 100644 --- a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp +++ b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp @@ -2,19 +2,40 @@ #include "../../../../Compositor.hpp" #include "../../../../helpers/Monitor.hpp" +#include "../../../../managers/input/InputManager.hpp" +#include CCursorZoomTrackpadGesture::CCursorZoomTrackpadGesture(const std::string& first, const std::string& second) { - try { - m_zoomValue = std::stof(first); - } catch (...) { ; } + if (const auto n = Hyprutils::String::strToNumber(first); n) + m_zoomValue = n.value(); if (second == "mult") m_mode = MODE_MULT; + else if (second == "live") + m_mode = MODE_LIVE; } void CCursorZoomTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) { ITrackpadGesture::begin(e); + if (m_mode == MODE_LIVE) { + if (!e.pinch) + return; + + m_monitor = g_pCompositor->getMonitorFromCursor(); + if (!m_monitor) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + m_zoomBegin = std::clamp(PMONITOR->m_cursorZoom->value(), 1.0F, 100.0F); + PMONITOR->m_cursorZoom->setValueAndWarp(m_zoomBegin); + PMONITOR->m_zoomController.pinAnchor(g_pInputManager->getMouseCoordsInternal() - PMONITOR->m_position); + return; + } + if (m_mode == MODE_TOGGLE) m_zoomed = !m_zoomed; @@ -25,9 +46,35 @@ void CCursorZoomTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureB *m->m_cursorZoom = m_zoomed ? m_zoomValue : *PZOOMFACTOR; break; case MODE_MULT: *m->m_cursorZoom = std::clamp(m->m_cursorZoom->goal() * m_zoomValue, 1.0F, 100.0F); break; + case MODE_LIVE: break; } } } -void CCursorZoomTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) {} -void CCursorZoomTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) {} +void CCursorZoomTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) { + if (m_mode != MODE_LIVE || !m_monitor || !e.pinch) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + auto zoom = std::clamp(m_zoomBegin * static_cast(e.pinch->scale), 1.0F, 100.0F); + + if (zoom < 1.05F) + zoom = 1.0F; + + PMONITOR->m_cursorZoom->setValueAndWarp(zoom); +} + +void CCursorZoomTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) { + if (m_mode != MODE_LIVE || !m_monitor) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + PMONITOR->m_zoomController.clearAnchor(); + m_monitor.reset(); +} diff --git a/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp b/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp index b53c81e98..3def907ff 100644 --- a/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp +++ b/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp @@ -2,6 +2,8 @@ #include "ITrackpadGesture.hpp" +#include "../../../../desktop/DesktopTypes.hpp" + class CCursorZoomTrackpadGesture : public ITrackpadGesture { public: CCursorZoomTrackpadGesture(const std::string& zoomLevel, const std::string& mode); @@ -14,10 +16,13 @@ class CCursorZoomTrackpadGesture : public ITrackpadGesture { private: float m_zoomValue = 1.0; inline static bool m_zoomed = false; + PHLMONITORREF m_monitor; + float m_zoomBegin = 1.0; enum eMode : uint8_t { MODE_TOGGLE = 0, MODE_MULT, + MODE_LIVE, }; eMode m_mode = MODE_TOGGLE; From c065e951d631a3aa3736b45f17b3556597f63c1a Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 2 May 2026 00:26:35 +0100 Subject: [PATCH 26/48] layout/dwindle,master: return invalid layoutmsg errors --- src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp | 3 ++- src/layout/algorithm/tiled/master/MasterAlgorithm.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp index eabb77674..100856be3 100644 --- a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp @@ -729,7 +729,8 @@ Config::ErrorResult CDwindleAlgorithm::layoutMsg(const std::string_view& sv) { CURRENT_NODE->pParent->splitRatio = std::clamp(newRatio, 0.1F, 1.9F); CURRENT_NODE->pParent->recalcSizePosRecursive(); - } + } else + return Config::configError(std::format("Unknown dwindle layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); return {}; } diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index a6849d2ac..a0ea13c34 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -777,7 +777,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); - } + } else + return Config::configError(std::format("Unknown master layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); return {}; } From c3e07986cc1afad0e69d30ac7817d7831f2d1c22 Mon Sep 17 00:00:00 2001 From: Tom Englund Date: Sat, 2 May 2026 17:24:20 +0200 Subject: [PATCH 27/48] renderer: only set presentationmode when required (#14252) if guard the setPresentationMode with the current state, shows up in profiling as minor waste on each frame. --- src/render/Renderer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index b5a32a2b9..6800c1857 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -2145,8 +2145,9 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { Event::bus()->m_events.render.stage.emit(RENDER_POST); pMonitor->m_output->state->addDamage(frameDamage); - pMonitor->m_output->state->setPresentationMode(shouldTear ? Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_IMMEDIATE : - Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_VSYNC); + auto presentationMode = shouldTear ? Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_IMMEDIATE : Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_VSYNC; + if (pMonitor->m_output->state->state().presentationMode != presentationMode) + pMonitor->m_output->state->setPresentationMode(presentationMode); if (commit) commitPendingAndDoExplicitSync(pMonitor); From fceb15979e487ed74f73e05caf786a91d45b4075 Mon Sep 17 00:00:00 2001 From: Gregor Date: Sat, 2 May 2026 18:22:02 +0200 Subject: [PATCH 28/48] config/lua: workspace.move/rename should accept "workspace" instead of "id" as a parameter (#14232) --- src/config/lua/bindings/LuaBindingsDispatchers.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 0b6ca07a0..4a981f4eb 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1167,9 +1167,9 @@ static int hlWorkspaceToggleSpecial(lua_State* L) { static int hlWorkspaceRename(lua_State* L) { if (!lua_istable(L, 1)) - return Internal::configError(L, "hl.workspace.rename: expected a table { id, name? }"); + return Internal::configError(L, "hl.workspace.rename: expected a table { workspace, name? }"); - const auto id = Internal::requireTableFieldWorkspaceSelector(L, 1, "id", "hl.workspace.rename"); + const auto id = Internal::requireTableFieldWorkspaceSelector(L, 1, "workspace", "hl.workspace.rename"); auto name = Internal::tableOptStr(L, 1, "name"); lua_pushstring(L, id.c_str()); @@ -1187,7 +1187,7 @@ static int hlWorkspaceMove(lua_State* L) { const auto mon = Internal::requireTableFieldMonitorSelector(L, 1, "monitor", "hl.workspace.move"); - auto id = Internal::tableOptWorkspaceSelector(L, 1, "id", "hl.workspace.move"); + auto id = Internal::tableOptWorkspaceSelector(L, 1, "workspace", "hl.workspace.move"); if (id) { lua_pushstring(L, id->c_str()); lua_pushstring(L, mon.c_str()); From a3fa7b49503d8eadd644d68c5bc452b600a11935 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 2 May 2026 19:18:44 +0100 Subject: [PATCH 29/48] keybinds: fix keycode matching on lua (#14254) --- src/config/legacy/ConfigManager.cpp | 8 +-- .../lua/bindings/LuaBindingsToplevel.cpp | 23 ++++++-- src/managers/KeybindManager.cpp | 56 +++++++++++++------ src/managers/KeybindManager.hpp | 12 ++-- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 2705c3959..1b18b4a7d 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1538,15 +1538,15 @@ std::optional CConfigManager::handleBind(const std::string& command else if ((ARGS.size() > sc(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse)) return "bind: too many args"; - std::vector KEYSYMS; - std::vector MODS; + std::vector KEYSYMS; + std::vector MODS; if (multiKey) { for (const auto& splitKey : CVarList(ARGS[1], 8, '&')) { - KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } for (const auto& splitMod : CVarList(ARGS[0], 8, '&')) { - MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } } const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 8bdd1c193..2d54a7b3c 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -9,6 +9,7 @@ #include "../../../devices/IKeyboard.hpp" #include "../../../managers/eventLoop/EventLoopManager.hpp" +#include #include #include @@ -46,12 +47,12 @@ static bool isSymSpecial(std::string_view sv) { } static std::expected parseKeyString(SKeybind& kb, std::string_view sv) { - bool modsEnded = false, specialSym = false; - CVarList2 vl(sv, 0, '+', true); + bool modsEnded = false, specialSym = false; + CVarList2 vl(sv, 0, '+', true); - uint32_t modMask = 0; - std::vector keysyms; - std::string lastKeyArg; + uint32_t modMask = 0; + std::vector> keysyms; + std::string lastKeyArg; if (sv == "catchall") { kb.catchAll = true; @@ -86,6 +87,16 @@ static std::expected parseKeyString(SKeybind& kb, std::string continue; } + if (arg.starts_with("code:") && isNumber(std::string{arg.substr(5)})) { + auto res = strToNumber(arg.substr(5)); + + if (!res) + return std::unexpected(std::format("Invalid keycode: \"{}\".", arg)); + + keysyms.emplace_back(XKB_KEY_NoSymbol, xkb_keycode_t{*res}); + continue; + } + auto sym = xkb_keysym_from_name(std::string{arg}.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); if (sym == XKB_KEY_NoSymbol) { @@ -99,7 +110,7 @@ static std::expected parseKeyString(SKeybind& kb, std::string } lastKeyArg = arg; - keysyms.emplace_back(sym); + keysyms.emplace_back(sym, 0); } kb.modmask = modMask; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 4a5df4be7..2496a0a50 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -505,25 +505,44 @@ void CKeybindManager::onSwitchOffEvent(const std::string& switchName) { handleKeybinds(0, SPressedKeyWithMods{.keyName = "switch:off:" + switchName}, true, nullptr, nullptr); } -eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector keybindKeysyms, const std::set pressedKeysyms) { - // Returns whether two sets of keysyms are equal, partially equal, or not - // matching. (Partially matching means that pressed is a subset of bound) +eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector& keybindKeysyms, const std::set& pressedKeysyms) { + // Returns whether the bound and pressed keys match fully, partially, or not at all. + // KeybindKey stores {keysym, keycode}; either non-zero field matching is enough. - std::set boundKeysNotPressed; - std::set pressedKeysNotBound; + const auto MATCHES = [](const KeybindKey& lhs, const KeybindKey& rhs) { + return (lhs.first != 0 && rhs.first != 0 && lhs.first == rhs.first) || (lhs.second != 0 && rhs.second != 0 && lhs.second == rhs.second); + }; - std::set symsKb; - for (const auto& k : keybindKeysyms) { - symsKb.emplace(k); + std::vector pressed{pressedKeysyms.begin(), pressedKeysyms.end()}; + std::vector boundForPressed(pressed.size(), -1); + + const auto tryMatch = [&](auto&& self, const size_t boundIdx, std::vector& seen) -> bool { + for (size_t pressedIdx = 0; pressedIdx < pressed.size(); ++pressedIdx) { + if (seen[pressedIdx] || !MATCHES(keybindKeysyms[boundIdx], pressed[pressedIdx])) + continue; + + seen[pressedIdx] = true; + + if (boundForPressed[pressedIdx] == -1 || self(self, static_cast(boundForPressed[pressedIdx]), seen)) { + boundForPressed[pressedIdx] = static_cast(boundIdx); + return true; + } + } + + return false; + }; + + size_t matches = 0; + for (size_t boundIdx = 0; boundIdx < keybindKeysyms.size(); ++boundIdx) { + std::vector seen(pressed.size(), false); + if (tryMatch(tryMatch, boundIdx, seen)) + ++matches; } - std::ranges::set_difference(symsKb, pressedKeysyms, std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); - std::ranges::set_difference(pressedKeysyms, symsKb, std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); - - if (boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches == keybindKeysyms.size() && matches == pressed.size()) return MK_FULL_MATCH; - if (!boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches > 0 || (pressed.empty() && !keybindKeysyms.empty())) return MK_PARTIAL_MATCH; return MK_NO_MATCH; @@ -556,14 +575,14 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP if (key.keysym != 0) { if (pressed) { if (keycodeToModifier(key.keycode)) - m_mkMods.insert(key.keysym); + m_mkMods.emplace(key.keysym, key.keycode); else - m_mkKeys.insert(key.keysym); + m_mkKeys.emplace(key.keysym, key.keycode); } else { if (keycodeToModifier(key.keycode)) - m_mkMods.erase(key.keysym); + std::erase_if(m_mkMods, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); else - m_mkKeys.erase(key.keysym); + std::erase_if(m_mkKeys, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); } } @@ -615,7 +634,8 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP // check for just the one match // this is also needed for multi-key binds so that SUPER + A + K can't // be actuated by SUPER + K + A - if (key.keysym != k->sMkKeys.back()) + auto& back = k->sMkKeys.back(); + if (key.keysym != back.first && key.keycode != back.second) continue; } else if (!key.keyName.empty()) { if (key.keyName != k->key) diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index 87cae14af..8c8855801 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -25,13 +25,15 @@ struct SSubmap { } }; +using KeybindKey = std::pair; + struct SKeybind { std::string key = ""; - std::vector sMkKeys = {}; + std::vector sMkKeys = {}; uint32_t keycode = 0; bool catchAll = false; uint32_t modmask = 0; - std::vector sMkMods = {}; + std::vector sMkMods = {}; std::string handler = ""; std::string arg = ""; bool locked = false; @@ -156,10 +158,10 @@ class CKeybindManager { SDispatchResult handleKeybinds(const uint32_t, const SPressedKeyWithMods&, bool, SP, SP); - std::set m_mkKeys = {}; - std::set m_mkMods = {}; + std::set m_mkKeys = {}; + std::set m_mkMods = {}; eMultiKeyCase mkBindMatches(const SP); - eMultiKeyCase mkKeysymSetMatches(const std::vector, const std::set); + eMultiKeyCase mkKeysymSetMatches(const std::vector&, const std::set&); bool handleInternalKeybinds(xkb_keysym_t); bool handleVT(xkb_keysym_t); From 30cd345addf0412baf87b71ad7213508c534e334 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 2 May 2026 19:23:09 +0100 Subject: [PATCH 30/48] hyprctl: fix bools in getoption --- src/debug/HyprCtl.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index e84a55527..b6777de36 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1632,6 +1632,8 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re if (format == FORMAT_NORMAL) { if (TYPE == typeid(Config::INTEGER)) return std::format("int: {}\nset: {}", **rc(VAL), VAR.setByUser); + else if (TYPE == typeid(Config::BOOL)) + return std::format("bool: {}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::FLOAT)) return std::format("float: {:2f}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::VEC2)) @@ -1655,6 +1657,8 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re } else { if (TYPE == typeid(Config::INTEGER)) return std::format(R"({{"option": "{}", "int": {}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); + else if (TYPE == typeid(Config::BOOL)) + return std::format(R"({{"option": "{}", "bool": {}, "set": {} }})", curitem, (**rc(VAL)) ? "true" : "false", VAR.setByUser); else if (TYPE == typeid(Config::FLOAT)) return std::format(R"({{"option": "{}", "float": {:2f}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::VEC2)) From e180c59b467bad2002efc7770acd4bfd5437da1b Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sat, 2 May 2026 20:34:43 +0100 Subject: [PATCH 31/48] desktop/windowRule: add `confine_pointer` window rule (#13379) --- .../lua/bindings/LuaBindingsInternal.hpp | 1 + .../rule/windowRule/WindowRuleApplicator.cpp | 11 +++- .../rule/windowRule/WindowRuleApplicator.hpp | 1 + .../windowRule/WindowRuleEffectContainer.cpp | 3 +- .../windowRule/WindowRuleEffectContainer.hpp | 1 + src/managers/input/InputManager.cpp | 63 ++++++++++++------- 6 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 0cced6b47..024d72489 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -101,6 +101,7 @@ namespace Config::Lua::Bindings::Internal { {"no_screen_share", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_SCREEN_SHARE}, {"no_vrr", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_VRR}, {"stay_focused", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_STAY_FOCUSED}, + {"confine_pointer", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_CONFINE_POINTER}, }; std::string argStr(lua_State* L, int idx); diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp index c19c63389..b3349bd6e 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp @@ -52,9 +52,9 @@ std::unordered_set CWindowRuleApplicato std::pair{std::ref(m_noFollowMouse), [this] { return noFollowMouseEffect(); }}, std::pair{std::ref(m_noScreenShare), [this] { return noScreenShareEffect(); }}, std::pair{std::ref(m_noVRR), [this] { return noVRREffect(); }}, std::pair{std::ref(m_persistentSize), [this] { return persistentSizeEffect(); }}, std::pair{std::ref(m_stayFocused), [this] { return stayFocusedEffect(); }}, std::pair{std::ref(m_idleInhibitMode), [this] { return idleInhibitModeEffect(); }}, - std::pair{std::ref(m_borderSize), [this] { return borderSizeEffect(); }}, std::pair{std::ref(m_rounding), [this] { return roundingEffect(); }}, - std::pair{std::ref(m_roundingPower), [this] { return roundingPowerEffect(); }}, std::pair{std::ref(m_scrollMouse), [this] { return scrollMouseEffect(); }}, - std::pair{std::ref(m_scrollTouchpad), [this] { return scrollTouchpadEffect(); }}, + std::pair{std::ref(m_confinePointer), [this] { return confinePointerEffect(); }}, std::pair{std::ref(m_borderSize), [this] { return borderSizeEffect(); }}, + std::pair{std::ref(m_rounding), [this] { return roundingEffect(); }}, std::pair{std::ref(m_roundingPower), [this] { return roundingPowerEffect(); }}, + std::pair{std::ref(m_scrollMouse), [this] { return scrollMouseEffect(); }}, std::pair{std::ref(m_scrollTouchpad), [this] { return scrollTouchpadEffect(); }}, std::pair{std::ref(m_animationStyle), [this] { return animationStyleEffect(); }}, std::pair{std::ref(m_maxSize), [this] { return maxSizeEffect(); }}, std::pair{std::ref(m_minSize), [this] { return minSizeEffect(); }}, std::pair{std::ref(m_activeBorderColor), [this] { return activeBorderColorEffect(); }}, std::pair{std::ref(m_inactiveBorderColor), [this] { return inactiveBorderColorEffect(); }})); @@ -342,6 +342,11 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const m_stayFocused.second |= rule->getPropertiesMask(); break; } + case WINDOW_RULE_EFFECT_CONFINE_POINTER: { + m_confinePointer.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_confinePointer.second |= rule->getPropertiesMask(); + break; + } case WINDOW_RULE_EFFECT_SCROLL_MOUSE: { m_scrollMouse.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_scrollMouse.second |= rule->getPropertiesMask(); diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp index f5e436092..2c382da8e 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp @@ -116,6 +116,7 @@ namespace Desktop::Rule { DEFINE_PROP(bool, noVRR, false, WINDOW_RULE_EFFECT_NO_VRR) DEFINE_PROP(bool, persistentSize, false, WINDOW_RULE_EFFECT_PERSISTENT_SIZE) DEFINE_PROP(bool, stayFocused, false, WINDOW_RULE_EFFECT_STAY_FOCUSED) + DEFINE_PROP(bool, confinePointer, false, WINDOW_RULE_EFFECT_CONFINE_POINTER) DEFINE_PROP(int, idleInhibitMode, false, WINDOW_RULE_EFFECT_IDLE_INHIBIT) diff --git a/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp b/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp index 668672477..3b624c03e 100644 --- a/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp +++ b/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp @@ -65,12 +65,13 @@ static const std::vector EFFECT_STRINGS = { "scroll_mouse", // "scroll_touchpad", // "stay_focused", // + "confine_pointer", // "__internal_last_static", // }; // This is here so that if we change the rules, we get reminded to update // the strings. -static_assert(WINDOW_RULE_EFFECT_LAST_STATIC == 55); +static_assert(WINDOW_RULE_EFFECT_LAST_STATIC == 56); CWindowRuleEffectContainer::CWindowRuleEffectContainer() : IEffectContainer(std::vector{EFFECT_STRINGS}) { ; diff --git a/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp b/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp index af8611090..b482c18e8 100644 --- a/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp +++ b/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp @@ -66,6 +66,7 @@ namespace Desktop::Rule { WINDOW_RULE_EFFECT_SCROLL_MOUSE, WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD, WINDOW_RULE_EFFECT_STAY_FOCUSED, + WINDOW_RULE_EFFECT_CONFINE_POINTER, WINDOW_RULE_EFFECT_LAST_STATIC, }; diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 7ed7b4f6b..2f9b64a3b 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -265,31 +265,50 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st g_pCompositor->scheduleFrameForMonitor(PMONITOR, Aquamarine::IOutput::AQ_SCHEDULE_CURSOR_MOVE); // constraints - if (!overridePos.has_value() && !g_pSeatManager->m_mouse.expired() && isConstrained()) { - const auto SURF = Desktop::View::CWLSurface::fromResource(Desktop::focusState()->surface()); - const auto CONSTRAINT = SURF ? SURF->constraint() : nullptr; + auto confineToRegion = [&](const CRegion& rg, SP surf) { + const auto CLOSEST = rg.closestPoint(mouseCoords); + const auto BOX = surf->getSurfaceBoxGlobal(); + const auto WINDOW = Desktop::View::CWindow::fromView(surf->view()); + const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); - if (CONSTRAINT) { - if (CONSTRAINT->isLocked()) { - const auto HINT = CONSTRAINT->logicPositionHint(); - g_pCompositor->warpCursorTo(HINT, true); + g_pCompositor->warpCursorTo(CLOSEST, true); + g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); + PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); + }; + + if (!g_pSeatManager->m_mouse.expired()) { + const auto SURF = Desktop::View::CWLSurface::fromResource(Desktop::focusState()->surface()); + + if (isConstrained()) { + const auto CONSTRAINT = SURF ? SURF->constraint() : nullptr; + + if (CONSTRAINT) { + if (CONSTRAINT->isLocked()) { + const auto HINT = CONSTRAINT->logicPositionHint(); + g_pCompositor->warpCursorTo(HINT, true); + } else { + confineToRegion(CONSTRAINT->logicConstraintRegion(), SURF); + } + + return; } else { - const auto RG = CONSTRAINT->logicConstraintRegion(); - const auto CLOSEST = RG.closestPoint(mouseCoords); - const auto BOX = SURF->getSurfaceBoxGlobal(); - const auto WINDOW = Desktop::View::CWindow::fromView(SURF->view()); - const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); - - g_pCompositor->warpCursorTo(CLOSEST, true); - g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); - PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); + Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc(SURF.get()), + rc(CONSTRAINT.get())); } - - return; - - } else - Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc(SURF.get()), - rc(CONSTRAINT.get())); + } else { + const auto WINDOW = SURF ? Desktop::View::CWindow::fromView(SURF->view()) : nullptr; + if (WINDOW) { + if (WINDOW->m_ruleApplicator->confinePointer().valueOrDefault()) { + const auto BOX = SURF->getSurfaceBoxGlobal(); + if (BOX.has_value()) { + CRegion rg; + rg.set(*BOX); + confineToRegion(rg, SURF); + } + return; + } + } + } } if (PMONITOR != Desktop::focusState()->monitor() && (*PMOUSEFOCUSMON || refocus) && m_forcedFocus.expired()) From f60e50da007aaf55a24d5e25e7a761f3ead51aaa Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Sat, 2 May 2026 22:36:14 +0300 Subject: [PATCH 32/48] internal: improve cursor size logging (#14180) --- src/managers/CursorManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/CursorManager.cpp b/src/managers/CursorManager.cpp index 2bf72f848..22b39e379 100644 --- a/src/managers/CursorManager.cpp +++ b/src/managers/CursorManager.cpp @@ -79,7 +79,7 @@ CCursorManager::CCursorManager() { if (SIZE) { try { m_size = std::stoi(SIZE); - } catch (...) { ; } + } catch (...) { Log::logger->log(Log::WARN, "Invalid HYPRCURSOR_SIZE value \"{}\"", SIZE); } } if (m_size <= 0) { @@ -93,7 +93,7 @@ CCursorManager::CCursorManager() { if (SIZE) { try { m_size = std::stoi(SIZE); - } catch (...) { ; } + } catch (...) { Log::logger->log(Log::WARN, "Invalid XCURSOR_SIZE value \"{}\"", SIZE); } } if (m_size <= 0) { From 334b361c8d1bf17652663768fd9237b46287781f Mon Sep 17 00:00:00 2001 From: Tony Miller <2032667+tnymlr@users.noreply.github.com> Date: Sun, 3 May 2026 06:06:00 +1000 Subject: [PATCH 33/48] screenshare: round captureBox after scaling to fix region capture at fractional scales (#14257) After scaling m_captureBox from logical to pixel coordinates, the box may have non-integer dimensions (e.g. logical 401x301 at scale 1.25 -> pixel 501.25x376.25). m_bufferSize is then computed as captureBox.size() and sent to the client as int32 width/height (truncating the fraction), but m_bufferSize itself stays as a fractional Vector2D. When the client allocates the integer-sized buffer and submits it, CScreenshareFrame::share() rejects it with ERROR_BUFFER_SIZE because Vector2D::operator== is exact double comparison: (501, 376) != (501.25, 376.25). The frame is sent failed, and the client retries forever. The SHARE_WINDOW path already rounds its bufferSize; the SHARE_REGION path didn't. Round the captureBox immediately after scaling so all downstream consumers (m_bufferSize, render translates) see clean integer pixel coordinates. Reproducer at scale 1.25 on a 1920x1080 monitor: wf-recorder -g '500,200 401x301' -f /tmp/x.mp4 # "Failed to copy frame, retrying..." until exit wf-recorder -g '500,200 400x300' -f /tmp/x.mp4 # works (400*1.25=500, 300*1.25=375, both integer) --- src/managers/screenshare/ScreenshareSession.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/managers/screenshare/ScreenshareSession.cpp b/src/managers/screenshare/ScreenshareSession.cpp index 1f7aeb881..7e6aea6fa 100644 --- a/src/managers/screenshare/ScreenshareSession.cpp +++ b/src/managers/screenshare/ScreenshareSession.cpp @@ -80,8 +80,9 @@ void CScreenshareSession::init() { if (g_pEventLoopManager) g_pEventLoopManager->addTimer(m_shareStopTimer); - // scale capture box since it's in logical coords - m_captureBox.scale(monitor()->m_scale); + // scale capture box since it's in logical coords; round to integer pixel + // dims so m_bufferSize matches the int32 size we send to the client + m_captureBox.scale(monitor()->m_scale).round(); m_listeners.monitorDestroyed = monitor()->m_events.disconnect.listen([this]() { stop(); }); m_listeners.monitorModeChanged = monitor()->m_events.modeChanged.listen([this]() { From 5202ab22f53c3298d599dbae0781037a1b1184d7 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 2 May 2026 21:06:10 +0100 Subject: [PATCH 34/48] cmakelists: fixup errors failing build on arch ci (#14259) --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1ffc6b3a..7b0c4fa63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,8 @@ add_compile_options( -Wall -Wextra -Wpedantic + -Wno-keyword-macro + -Wno-unused-result -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers From fe17cea39ed9e001b61035e8edbbf2f12871659b Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Sat, 2 May 2026 16:41:29 -0400 Subject: [PATCH 35/48] layout/master: fix rollprev/rollnext focusing the wrong window (#14209) * layout/master: fix rollprev/rollnext focusing the wrong window * tests/master: add tests for rollnext/rollprev focus --- hyprtester/src/tests/main/master.cpp | 69 +++++++++++++++++++ .../tiled/master/MasterAlgorithm.cpp | 16 +++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/hyprtester/src/tests/main/master.cpp b/hyprtester/src/tests/main/master.cpp index b9364c455..806cbdf70 100644 --- a/hyprtester/src/tests/main/master.cpp +++ b/hyprtester/src/tests/main/master.cpp @@ -1,6 +1,7 @@ #include "../shared.hpp" #include "../../shared.hpp" #include "../../hyprctlCompat.hpp" +#include #include "tests.hpp" TEST_CASE(focusMasterPrevious) { @@ -141,3 +142,71 @@ TEST_CASE(fsBehavior) { EXPECT_CONTAINS(str, "fullscreen: 0"); } } + +TEST_CASE(rollFocus) { + // test rollnext/rollprev dispatchers + + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); + + // set up windows + std::vector windows = {"slave1", "slave2", "slave3", "master"}; + + // helper lambda thing + auto roll = [&](const std::string& dir) { + auto pivot = (dir == "rollnext") ? windows.begin() + 1 : windows.end() - 1; + + // rotate the windows vector along with the actual windows + // the rolling behavior of the window focus should follow the + // rotating behavior of std::ranges::rotate + OK(getFromSocket("/dispatch hl.dsp.layout('" + dir + "')")); + std::ranges::rotate(windows.begin(), pivot, windows.end()); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: " + windows.back()); + }; + + for (auto const& win : windows) { + if (!Tests::spawnKitty(win)) { + FAIL_TEST("Could not spawn kitty with win class `{}`", win); + } + } + + // focus master + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster master')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: master"); + + // put the windows in the washing machine + NLog::log("{}Testing rollnext", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + roll("rollnext"); + } + + NLog::log("{}Testing rollprev", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + roll("rollprev"); + } + + NLog::log("{}Testing rollnext with rollprev", Colors::YELLOW); + for (int i = 0; i < 10; ++i) { + for (int j = 0; j < 5; ++j) { + roll("rollnext"); + } + roll("rollprev"); + } + + NLog::log("{}Testing rollnext/rollprev alternation", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + if (i % 2 == 0) { + roll("rollnext"); + } else { + roll("rollprev"); + } + } + + NLog::log("{}Testing rollnext/rollprev burst calls", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + if (i / 5 % 2 == 0) { + roll("rollnext"); + } else { + roll("rollprev"); + } + } +} diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index a0ea13c34..c57a3639f 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -715,12 +715,15 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { if (!OLDMASTER) return stateErr("no old master"); - auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + SP newFocus; for (auto& nd : m_masterNodesData) { if (!nd->isMaster) { const auto& newMaster = nd; newMaster->isMaster = true; + newFocus = newMaster->pTarget.lock(); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); @@ -729,7 +732,6 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { else if (newMasterIt > oldMasterIt) std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); - switchToWindow(newMaster->pTarget.lock()); OLDMASTER->isMaster = false; oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -741,6 +743,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); + if (newFocus) + switchToWindow(newFocus); } else if (command == "rollprev") { const auto PNODE = getNodeFromWindow(PWINDOW); @@ -751,12 +755,15 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { if (!OLDMASTER) return stateErr("no old master"); - auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + SP newFocus; for (auto& nd : m_masterNodesData | std::views::reverse) { if (!nd->isMaster) { const auto& newMaster = nd; newMaster->isMaster = true; + newFocus = newMaster->pTarget.lock(); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); @@ -765,7 +772,6 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { else if (newMasterIt > oldMasterIt) std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); - switchToWindow(newMaster->pTarget.lock()); OLDMASTER->isMaster = false; oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -777,6 +783,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); + if (newFocus) + switchToWindow(newFocus); } else return Config::configError(std::format("Unknown master layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); From 6ec0228c38a6203e4789fe7e7e793a558521c109 Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sat, 2 May 2026 23:12:49 +0100 Subject: [PATCH 36/48] desktop/windowRule: add parser switch for confine pointer (#14263) --- src/desktop/rule/windowRule/WindowRule.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/desktop/rule/windowRule/WindowRule.cpp b/src/desktop/rule/windowRule/WindowRule.cpp index 10d39383b..5bbf2abe4 100644 --- a/src/desktop/rule/windowRule/WindowRule.cpp +++ b/src/desktop/rule/windowRule/WindowRule.cpp @@ -248,6 +248,7 @@ static std::expected parseWindowRuleEffect(C case WINDOW_RULE_EFFECT_RENDER_UNFOCUSED: case WINDOW_RULE_EFFECT_NO_SCREEN_SHARE: case WINDOW_RULE_EFFECT_NO_VRR: + case WINDOW_RULE_EFFECT_CONFINE_POINTER: case WINDOW_RULE_EFFECT_STAY_FOCUSED: return truthy(raw); case WINDOW_RULE_EFFECT_FULLSCREENSTATE: { From aa5e38041ed332416ac2f5ace908389c35b28192 Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PointerDilemma@users.noreply.github.com> Date: Sun, 3 May 2026 15:58:03 +0200 Subject: [PATCH 37/48] sessionLock: send locked instead of denied when missing a lock frame for 5 seconds (#14271) The protocol seems to allow this, because it explicitly mentions either session lock surface or compositor blanking the output in this sentence: > The locked event must not be sent until a new "locked" frame (either from a session lock surface or the compositor blanking the output) has been presented on all outputs and no security sensitive normal/unlocked content is possibly visible. Since `locked_screen_delay` is capped to 5 seconds, after the 5 seconds this timer is registered for, we assume that all outputs are covered either by lockedead or by an actual lock frame. Thus sending locked is ok. --- src/managers/SessionLockManager.cpp | 33 ++++++++++++----------------- src/managers/SessionLockManager.hpp | 4 ++-- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/managers/SessionLockManager.cpp b/src/managers/SessionLockManager.cpp index 8890e2e07..6e45c9234 100644 --- a/src/managers/SessionLockManager.cpp +++ b/src/managers/SessionLockManager.cpp @@ -105,8 +105,10 @@ void CSessionLockManager::onNewSessionLock(SP pLock) { return; } - m_sessionLock->sendDeniedTimer = makeShared( - // Within this arbitrary amount of time, a session-lock client is expected to create and commit a lock surface for each output. If the client fails to do that, it will be denied. + m_sessionLock->sendLockedTimer = makeShared( + // Clients get sent the "locked" event after they submitted a lock frame for each output. + // If they fail to do this, we send the "locked" event after a fixed amount of time here. + // Previously we sent denied after this timeout, but that forcefully makes the client exit and the protocol doesn't require that anyways. std::chrono::seconds(5), [](auto, auto) { if (!g_pSessionLockManager || g_pSessionLockManager->clientLocked() || g_pSessionLockManager->clientDenied()) @@ -115,29 +117,22 @@ void CSessionLockManager::onNewSessionLock(SP pLock) { if (!g_pSessionLockManager->m_sessionLock || !g_pSessionLockManager->m_sessionLock->lock) return; - if (g_pCompositor->m_unsafeState || !g_pCompositor->m_aqBackend->hasSession() || !g_pCompositor->m_aqBackend->session->active) { - // Because the session is inactive, there is a good reason for why the client did't manage to render to all outputs. - // We send locked, although this could lead to imperfect frames when we start to render again. - g_pSessionLockManager->m_sessionLock->lock->sendLocked(); - g_pSessionLockManager->m_sessionLock->hasSentLocked = true; - return; - } - - LOGM(Log::WARN, "Kicking lockscreen client, because it failed to render to all outputs within 5 seconds"); - g_pSessionLockManager->m_sessionLock->lock->sendDenied(); - g_pSessionLockManager->m_sessionLock->hasSentDenied = true; + LOGM(Log::WARN, + "Sending locked after a 5 second timeout. This happens when we failed to render a lock frame from the client for every output. Lockdead frames may be shown."); + g_pSessionLockManager->m_sessionLock->lock->sendLocked(); + g_pSessionLockManager->m_sessionLock->hasSentLocked = true; }, nullptr); - g_pEventLoopManager->addTimer(m_sessionLock->sendDeniedTimer); + g_pEventLoopManager->addTimer(m_sessionLock->sendLockedTimer); } -void CSessionLockManager::removeSendDeniedTimer() { - if (!m_sessionLock || !m_sessionLock->sendDeniedTimer) +void CSessionLockManager::removeSendLockedTimer() { + if (!m_sessionLock || !m_sessionLock->sendLockedTimer) return; - g_pEventLoopManager->removeTimer(m_sessionLock->sendDeniedTimer); - m_sessionLock->sendDeniedTimer.reset(); + g_pEventLoopManager->removeTimer(m_sessionLock->sendLockedTimer); + m_sessionLock->sendLockedTimer.reset(); } bool CSessionLockManager::isSessionLocked() { @@ -169,7 +164,7 @@ void CSessionLockManager::onLockscreenRenderedOnMonitor(uint64_t id) { std::ranges::all_of(g_pCompositor->m_monitors, [this](auto m) { return !m->m_enabled || !m->m_dpmsStatus || m_sessionLock->lockedMonitors.contains(m->m_id); }); if (LOCKED && m_sessionLock->lock->good()) { - removeSendDeniedTimer(); + removeSendLockedTimer(); m_sessionLock->lock->sendLocked(); m_sessionLock->hasSentLocked = true; } diff --git a/src/managers/SessionLockManager.hpp b/src/managers/SessionLockManager.hpp index efcaf09a7..dba2758ea 100644 --- a/src/managers/SessionLockManager.hpp +++ b/src/managers/SessionLockManager.hpp @@ -31,7 +31,7 @@ struct SSessionLockSurface { struct SSessionLock { WP lock; CTimer lockTimer; - SP sendDeniedTimer; + SP sendLockedTimer; std::vector> vSessionLockSurfaces; @@ -73,7 +73,7 @@ class CSessionLockManager { } m_listeners; void onNewSessionLock(SP pWlrLock); - void removeSendDeniedTimer(); + void removeSendLockedTimer(); }; inline UP g_pSessionLockManager; From 90fe7c6569e3ea900cdc664a13368e52c986165d Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sun, 3 May 2026 15:00:16 +0100 Subject: [PATCH 38/48] InputManager: add guards to confineToRegion to avoid issues (#14269) --- src/managers/input/InputManager.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 2f9b64a3b..b6f384337 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -266,11 +266,17 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st // constraints auto confineToRegion = [&](const CRegion& rg, SP surf) { + if (!surf) + return; + const auto CLOSEST = rg.closestPoint(mouseCoords); const auto BOX = surf->getSurfaceBoxGlobal(); const auto WINDOW = Desktop::View::CWindow::fromView(surf->view()); const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); + if (g_pSeatManager->m_state.pointerFocus != surf->resource()) + g_pSeatManager->setPointerFocus(surf->resource(), CLOSESTLOCAL); + g_pCompositor->warpCursorTo(CLOSEST, true); g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); From c0933bffcf0394f7701ab134f6c308eaa9584bee Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PointerDilemma@users.noreply.github.com> Date: Sun, 3 May 2026 16:30:21 +0200 Subject: [PATCH 39/48] compositor: move SessionLockManager init from STAGE_LATE to STAGE_BASICINIT (#14272) --- src/Compositor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index ab371df22..b5dba342e 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -673,6 +673,9 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the SeatManager!"); g_pSeatManager = makeUnique(); + Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!"); + g_pSessionLockManager = makeUnique(); + // init focus state els Desktop::History::windowTracker(); Desktop::History::workspaceTracker(); @@ -688,9 +691,6 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the XWaylandManager!"); g_pXWaylandManager = makeUnique(); - Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!"); - g_pSessionLockManager = makeUnique(); - Log::logger->log(Log::DEBUG, "Creating the Debug Overlay!"); Debug::overlay(); From 21fa9b2ee27227c964fdeb8090d98255edb89bec Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sun, 3 May 2026 15:41:44 +0100 Subject: [PATCH 40/48] config/lua: fix dispatcher shapes to not be callable (#14268) this would only lead to abuse, explicitly forbid it --- meta/generateLuaStubs.py | 9 +- meta/hl.meta.lua | 103 +++++++------ .../bindings/LuaBindingsDispatcherUtils.cpp | 144 ++++++++++++++++++ .../lua/bindings/LuaBindingsDispatchers.cpp | 5 + .../lua/bindings/LuaBindingsInternal.cpp | 5 - .../lua/bindings/LuaBindingsInternal.hpp | 3 + .../lua/bindings/LuaBindingsToplevel.cpp | 8 +- 7 files changed, 215 insertions(+), 62 deletions(-) create mode 100644 src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py index a523496df..7ef70babb 100644 --- a/meta/generateLuaStubs.py +++ b/meta/generateLuaStubs.py @@ -469,7 +469,8 @@ def generate_stub(root: Path) -> str: api_signatures: dict[str, str] = { "hl.on": "fun(event: HL.EventName, cb: fun(...)): HL.EventSubscription", - "hl.bind": "fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind", + "hl.bind": "fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind", + "hl.dispatch": "fun(dispatcher: HL.Dispatcher|function): any", "hl.define_submap": "fun(name: string, reset_or_fn: string|function, fn?: function): nil", "hl.timer": "fun(callback: function, opts: HL.TimerOptions): HL.Timer", "hl.config": "fun(config: table): nil", @@ -523,6 +524,9 @@ def generate_stub(root: Path) -> str: lines.append("---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer}") lines.append("---@alias HL.Gradient string|{colors:string[], angle?:number}") lines.append("") + lines.append("---@class HL.Dispatcher") + lines.append("local __HL_Dispatcher = {}") + lines.append("") lines.extend( emit_class_block( @@ -651,7 +655,8 @@ def generate_stub(root: Path) -> str: for method in sorted(node.methods): full_name = f"{full_prefix}.{method}" - method_type = api_signatures.get(full_name, "fun(...): any") + default_method_type = "fun(...): HL.Dispatcher" if path and path[0] == "dsp" else "fun(...): any" + method_type = api_signatures.get(full_name, default_method_type) fields.append((method, method_type, False)) for child_name in sorted(node.children.keys()): diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index e0fd096ae..a13655633 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -378,6 +378,9 @@ ---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer} ---@alias HL.Gradient string|{colors:string[], angle?:number} +---@class HL.Dispatcher +local __HL_Dispatcher = {} + ---@class HL.Vec2 ---@field x number ---@field y number @@ -739,12 +742,12 @@ local __HL_Workspace = {} ---@class HL.API ---@field animation fun(...): any ----@field bind fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind +---@field bind fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind ---@field config fun(config: table): nil ---@field curve fun(...): any ---@field define_submap fun(name: string, reset_or_fn: string|function, fn?: function): nil ---@field device fun(spec: HL.DeviceSpec): nil ----@field dispatch fun(...): any +---@field dispatch fun(dispatcher: HL.Dispatcher|function): any ---@field env fun(...): any ---@field exec_cmd fun(cmd: string, rules?: table): nil ---@field gesture fun(spec: HL.GestureSpec): nil @@ -783,21 +786,21 @@ local __HL_Workspace = {} local __HL_API = {} ---@class HL.DspNamespace ----@field dpms fun(...): any ----@field event fun(...): any ----@field exec_cmd fun(...): any ----@field exec_raw fun(...): any ----@field exit fun(...): any ----@field focus fun(...): any ----@field force_idle fun(...): any ----@field force_renderer_reload fun(...): any ----@field global fun(...): any ----@field layout fun(...): any ----@field no_op fun(...): any ----@field pass fun(...): any ----@field send_key_state fun(...): any ----@field send_shortcut fun(...): any ----@field submap fun(...): any +---@field dpms fun(...): HL.Dispatcher +---@field event fun(...): HL.Dispatcher +---@field exec_cmd fun(...): HL.Dispatcher +---@field exec_raw fun(...): HL.Dispatcher +---@field exit fun(...): HL.Dispatcher +---@field focus fun(...): HL.Dispatcher +---@field force_idle fun(...): HL.Dispatcher +---@field force_renderer_reload fun(...): HL.Dispatcher +---@field global fun(...): HL.Dispatcher +---@field layout fun(...): HL.Dispatcher +---@field no_op fun(...): HL.Dispatcher +---@field pass fun(...): HL.Dispatcher +---@field send_key_state fun(...): HL.Dispatcher +---@field send_shortcut fun(...): HL.Dispatcher +---@field submap fun(...): HL.Dispatcher ---@field cursor HL.DspCursorNamespace ---@field group HL.DspGroupNamespace ---@field window HL.DspWindowNamespace @@ -805,48 +808,48 @@ local __HL_API = {} local __HL_DspNamespace = {} ---@class HL.DspCursorNamespace ----@field move fun(...): any ----@field move_to_corner fun(...): any +---@field move fun(...): HL.Dispatcher +---@field move_to_corner fun(...): HL.Dispatcher local __HL_DspCursorNamespace = {} ---@class HL.DspGroupNamespace ----@field active fun(...): any ----@field lock fun(...): any ----@field lock_active fun(...): any ----@field move_window fun(...): any ----@field next fun(...): any ----@field prev fun(...): any ----@field toggle fun(...): any +---@field active fun(...): HL.Dispatcher +---@field lock fun(...): HL.Dispatcher +---@field lock_active fun(...): HL.Dispatcher +---@field move_window fun(...): HL.Dispatcher +---@field next fun(...): HL.Dispatcher +---@field prev fun(...): HL.Dispatcher +---@field toggle fun(...): HL.Dispatcher local __HL_DspGroupNamespace = {} ---@class HL.DspWindowNamespace ----@field alter_zorder fun(...): any ----@field bring_to_top fun(...): any ----@field center fun(...): any ----@field close fun(...): any ----@field cycle_next fun(...): any ----@field deny_from_group fun(...): any ----@field drag fun(...): any ----@field float fun(...): any ----@field fullscreen fun(...): any ----@field fullscreen_state fun(...): any ----@field kill fun(...): any ----@field move fun(...): any ----@field pin fun(...): any ----@field pseudo fun(...): any ----@field resize fun(...): any ----@field set_prop fun(...): any ----@field signal fun(...): any ----@field swap fun(...): any ----@field tag fun(...): any ----@field toggle_swallow fun(...): any +---@field alter_zorder fun(...): HL.Dispatcher +---@field bring_to_top fun(...): HL.Dispatcher +---@field center fun(...): HL.Dispatcher +---@field close fun(...): HL.Dispatcher +---@field cycle_next fun(...): HL.Dispatcher +---@field deny_from_group fun(...): HL.Dispatcher +---@field drag fun(...): HL.Dispatcher +---@field float fun(...): HL.Dispatcher +---@field fullscreen fun(...): HL.Dispatcher +---@field fullscreen_state fun(...): HL.Dispatcher +---@field kill fun(...): HL.Dispatcher +---@field move fun(...): HL.Dispatcher +---@field pin fun(...): HL.Dispatcher +---@field pseudo fun(...): HL.Dispatcher +---@field resize fun(...): HL.Dispatcher +---@field set_prop fun(...): HL.Dispatcher +---@field signal fun(...): HL.Dispatcher +---@field swap fun(...): HL.Dispatcher +---@field tag fun(...): HL.Dispatcher +---@field toggle_swallow fun(...): HL.Dispatcher local __HL_DspWindowNamespace = {} ---@class HL.DspWorkspaceNamespace ----@field move fun(...): any ----@field rename fun(...): any ----@field swap_monitors fun(...): any ----@field toggle_special fun(...): any +---@field move fun(...): HL.Dispatcher +---@field rename fun(...): HL.Dispatcher +---@field swap_monitors fun(...): HL.Dispatcher +---@field toggle_special fun(...): HL.Dispatcher local __HL_DspWorkspaceNamespace = {} ---@class HL.NotificationNamespace diff --git a/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp new file mode 100644 index 000000000..a55dfc655 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp @@ -0,0 +1,144 @@ +#include "LuaBindingsInternal.hpp" + +using namespace Config::Lua::Bindings; + +static constexpr const char* DISPATCHER_MT = "HL.Dispatcher"; +static char DISPATCHER_TABLES_REGISTRY_KEY; + +namespace { + struct SDispatcherRef { + int ref = LUA_NOREF; + }; +} + +static int dispatcherGc(lua_State* L) { + auto* dispatcher = sc(luaL_checkudata(L, 1, DISPATCHER_MT)); + if (dispatcher->ref != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, dispatcher->ref); + dispatcher->ref = LUA_NOREF; + } + + return 0; +} + +static int dispatcherCall(lua_State* L) { + return Internal::configError(L, "dispatcher objects cannot be called directly; use hl.dispatch(dispatcher)"); +} + +static int dispatcherToString(lua_State* L) { + lua_pushstring(L, "HL.Dispatcher"); + return 1; +} + +static void ensureDispatcherMetatable(lua_State* L) { + if (luaL_newmetatable(L, DISPATCHER_MT)) { + lua_pushcfunction(L, dispatcherGc); + lua_setfield(L, -2, "__gc"); + lua_pushcfunction(L, dispatcherCall); + lua_setfield(L, -2, "__call"); + lua_pushcfunction(L, dispatcherToString); + lua_setfield(L, -2, "__tostring"); + + lua_pushstring(L, DISPATCHER_MT); + lua_setfield(L, -2, "__metatable"); + } + + lua_pop(L, 1); +} + +static bool isDispatcherTable(lua_State* L, int idx) { + if (!lua_istable(L, idx)) + return false; + + idx = lua_absindex(L, idx); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pushvalue(L, idx); + lua_rawget(L, -2); + const bool result = lua_toboolean(L, -1); + lua_pop(L, 2); + return result; +} + +static int dispatcherFactory(lua_State* L) { + const int nargs = lua_gettop(L); + + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + lua_call(L, nargs, LUA_MULTRET); + + const int nresults = lua_gettop(L); + if (nresults == 1 && lua_isfunction(L, -1)) + return Internal::wrapDispatcher(L); + + return nresults; +} + +void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { + if (isDispatcherTable(L, -1)) { + lua_pushcfunction(L, fn); + lua_pushcclosure(L, dispatcherFactory, 1); + } else + lua_pushcfunction(L, fn); + + lua_setfield(L, -2, name); +} + +void Internal::markDispatcherTable(lua_State* L) { + if (!lua_istable(L, -1)) + return; + + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_pushvalue(L, -2); + lua_rawset(L, LUA_REGISTRYINDEX); + } + + lua_pushvalue(L, -2); + lua_pushboolean(L, true); + lua_rawset(L, -3); + lua_pop(L, 1); +} + +int Internal::wrapDispatcher(lua_State* L) { + luaL_checktype(L, -1, LUA_TFUNCTION); + + const int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + new (lua_newuserdata(L, sizeof(SDispatcherRef))) SDispatcherRef{.ref = ref}; + + ensureDispatcherMetatable(L); + luaL_getmetatable(L, DISPATCHER_MT); + lua_setmetatable(L, -2); + + return 1; +} + +bool Internal::pushDispatcherFunction(lua_State* L, int idx) { + if (lua_isfunction(L, idx)) { + lua_pushvalue(L, idx); + return true; + } + + auto* dispatcher = sc(luaL_testudata(L, idx, DISPATCHER_MT)); + if (!dispatcher || dispatcher->ref == LUA_NOREF) + return false; + + lua_rawgeti(L, LUA_REGISTRYINDEX, dispatcher->ref); + if (lua_isfunction(L, -1)) + return true; + + lua_pop(L, 1); + return false; +} diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 4a981f4eb..4fd332eb7 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1214,14 +1214,17 @@ static int hlWorkspaceSwapMonitors(lua_State* L) { void Internal::registerDispatcherBindings(lua_State* L) { lua_newtable(L); + Internal::markDispatcherTable(L); { lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "move_to_corner", hlCursorMoveToCorner); Internal::setFn(L, "move", hlCursorMove); lua_setfield(L, -2, "cursor"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "toggle", hlGroupToggle); Internal::setFn(L, "next", hlGroupNext); Internal::setFn(L, "prev", hlGroupPrev); @@ -1232,6 +1235,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "group"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "close", hlWindowClose); Internal::setFn(L, "kill", hlWindowKill); Internal::setFn(L, "signal", hlWindowSignal); @@ -1255,6 +1259,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "window"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "rename", hlWorkspaceRename); Internal::setFn(L, "move", hlWorkspaceMove); Internal::setFn(L, "swap_monitors", hlWorkspaceSwapMonitors); diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp index e0cef1464..5f1958051 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.cpp +++ b/src/config/lua/bindings/LuaBindingsInternal.cpp @@ -449,11 +449,6 @@ CA::eTogglableAction Internal::tableToggleAction(lua_State* L, int idx, const ch return CA::TOGGLE_ACTION_TOGGLE; } -void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { - lua_pushcfunction(L, fn); - lua_setfield(L, -2, name); -} - void Internal::setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn) { lua_pushlightuserdata(L, mgr); lua_pushcclosure(L, fn, 1); diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 024d72489..9b61360cb 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -181,6 +181,9 @@ namespace Config::Lua::Bindings::Internal { void setFn(lua_State* L, const char* name, lua_CFunction fn); void setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn); + void markDispatcherTable(lua_State* L); + int wrapDispatcher(lua_State* L); + bool pushDispatcherFunction(lua_State* L, int idx); template SParseError parseTableField(lua_State* L, int tableIdx, const char* field, T& parser) { diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 2d54a7b3c..1b345cdef 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -132,13 +132,12 @@ static int hlBind(lua_State* L) { if (auto res = parseKeyString(kb, keys); !res) return Internal::configError(L, std::format("hl.bind: failed to parse key string: {}", res.error())); - if (!lua_isfunction(L, 2)) + if (!Internal::pushDispatcherFunction(L, 2)) return Internal::configError(L, "hl.bind: dispatcher must be a dispatcher (e.g. hl.dsp.window.close()) or a lua function"); if (kb.catchAll && mgr->m_currentSubmap.empty()) return Internal::configError(L, "hl.bind: catchall keybinds are only allowed in submaps."); - lua_pushvalue(L, 2); int ref = luaL_ref(L, LUA_REGISTRYINDEX); kb.handler = "__lua"; kb.arg = std::to_string(ref); @@ -293,10 +292,9 @@ static int hlExecCmd(lua_State* L) { } static int hlDispatch(lua_State* L) { - if (!lua_isfunction(L, 1)) - return Internal::configError(L, "hl.dispatch: expected a dispatcher function (e.g. hl.dsp.window.close())"); + if (!Internal::pushDispatcherFunction(L, 1)) + return Internal::configError(L, "hl.dispatch: expected a dispatcher (e.g. hl.dsp.window.close())"); - lua_pushvalue(L, 1); int status = LUA_OK; if (auto* mgr = CConfigManager::fromLuaState(L); mgr) status = mgr->guardedPCall(0, 1, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, "hl.dispatch"); From 6a7abd00379b4f37b468bc2d0e717d4ba293efe1 Mon Sep 17 00:00:00 2001 From: Lichie <90825386+lichie567@users.noreply.github.com> Date: Sun, 3 May 2026 10:55:47 -0700 Subject: [PATCH 41/48] config/lua: add clear tag api (#14273) --- meta/hl.meta.lua | 1 + src/config/lua/bindings/LuaBindingsDispatchers.cpp | 11 +++++++++++ src/config/shared/actions/ConfigActions.cpp | 13 +++++++++++++ src/config/shared/actions/ConfigActions.hpp | 1 + src/helpers/TagKeeper.cpp | 9 +++++++++ src/helpers/TagKeeper.hpp | 1 + 6 files changed, 36 insertions(+) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index a13655633..d6a44567d 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -826,6 +826,7 @@ local __HL_DspGroupNamespace = {} ---@field alter_zorder fun(...): HL.Dispatcher ---@field bring_to_top fun(...): HL.Dispatcher ---@field center fun(...): HL.Dispatcher +---@field clear_tags fun(...): HL.Dispatcher ---@field close fun(...): HL.Dispatcher ---@field cycle_next fun(...): HL.Dispatcher ---@field deny_from_group fun(...): HL.Dispatcher diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 4fd332eb7..32b4e2bf2 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -563,6 +563,10 @@ static int dsp_tagWindow(lua_State* L) { return Internal::checkResult(L, CA::tag(lua_tostring(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); } +static int dsp_clearTags(lua_State* L) { + return Internal::checkResult(L, CA::clearTags(Internal::windowFromUpval(L, 1))); +} + static int dsp_toggleSwallow(lua_State* L) { return Internal::checkResult(L, CA::toggleSwallow()); } @@ -915,6 +919,12 @@ static int hlWindowTag(lua_State* L) { return 1; } +static int hlWindowClearTags(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_clearTags, 1); + return 1; +} + static int hlWindowToggleSwallow(lua_State* L) { lua_pushcclosure(L, dsp_toggleSwallow, 0); return 1; @@ -1248,6 +1258,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { Internal::setFn(L, "center", hlWindowCenter); Internal::setFn(L, "cycle_next", hlWindowCycleNext); Internal::setFn(L, "tag", hlWindowTag); + Internal::setFn(L, "clear_tags", hlWindowClearTags); Internal::setFn(L, "toggle_swallow", hlWindowToggleSwallow); Internal::setFn(L, "pin", hlWindowPin); Internal::setFn(L, "bring_to_top", hlWindowBringToTop); diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index 5db1aaf1e..7111afd8f 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -605,6 +605,19 @@ ActionResult Actions::tag(const std::string& tagStr, std::optional w) return {}; } +ActionResult Actions::clearTags(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->m_ruleApplicator->m_tagKeeper.clearTags()) { + window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_TAG); + window->updateDecorationValues(); + } + + return {}; +} + ActionResult Actions::swapNext(const bool next, std::optional w) { auto window = xtract(w); if (!window) diff --git a/src/config/shared/actions/ConfigActions.hpp b/src/config/shared/actions/ConfigActions.hpp index 0f16aff74..6ddf64d9d 100644 --- a/src/config/shared/actions/ConfigActions.hpp +++ b/src/config/shared/actions/ConfigActions.hpp @@ -53,6 +53,7 @@ namespace Config::Actions { ActionResult move(const Vector2D& pos, bool relative = false, std::optional window = std::nullopt /* Active */); ActionResult cycleNext(const bool next, std::optional onlyTiled, std::optional onlyFloating, std::optional window = std::nullopt /* Active */); ActionResult tag(const std::string& tag, std::optional window = std::nullopt /* Active */); + ActionResult clearTags(std::optional w = std::nullopt); ActionResult pass(std::optional window = std::nullopt /* Active */); ActionResult pass(uint32_t modMask, uint32_t key, std::optional window = std::nullopt /* Active */); ActionResult sendKeyState(uint32_t modMask, uint32_t key, uint32_t state, std::optional window = std::nullopt /* Active */); diff --git a/src/helpers/TagKeeper.cpp b/src/helpers/TagKeeper.cpp index 7f377657e..80bf3957d 100644 --- a/src/helpers/TagKeeper.cpp +++ b/src/helpers/TagKeeper.cpp @@ -38,6 +38,15 @@ bool CTagKeeper::applyTag(const std::string& tag, bool dynamic) { return true; } +bool CTagKeeper::clearTags() { + if (!m_tags.empty()) { + m_tags.clear(); + return true; + } + + return false; +} + bool CTagKeeper::removeDynamicTag(const std::string& s) { return std::erase_if(m_tags, [&s](const auto& tag) { return tag == s + "*"; }); } diff --git a/src/helpers/TagKeeper.hpp b/src/helpers/TagKeeper.hpp index d18a0d29a..f90b06295 100644 --- a/src/helpers/TagKeeper.hpp +++ b/src/helpers/TagKeeper.hpp @@ -8,6 +8,7 @@ class CTagKeeper { bool isTagged(const std::string& tag, bool strict = false) const; bool applyTag(const std::string& tag, bool dynamic = false); bool removeDynamicTag(const std::string& tag); + bool clearTags(); const auto& getTags() const { return m_tags; From 6d4bcaf075dec8f6de003cb8403df8c788e7079e Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Mon, 4 May 2026 02:07:41 +0530 Subject: [PATCH 42/48] tests: skip pointer tests in CI due to missing input environment (#14238) * tests: skip pointer tests when pointer input is non-functional * tests: skip pointer tests in CI due to unreliable cursor behavior * tests: skip pointer tests when no reliable input environment is available * tests: skip pointer tests when pointer behavior is unreliable * tests: temporarily disable pointer tests due to unstable CI environment * tests: enforce deterministic pointer behavior (flat accel + fixed sensitivity) * tests: temporarily disable pointer tests due to unstable CI environment --- hyprtester/src/tests/clients/pointer-scroll.cpp | 3 +++ hyprtester/src/tests/clients/pointer-warp.cpp | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/hyprtester/src/tests/clients/pointer-scroll.cpp b/hyprtester/src/tests/clients/pointer-scroll.cpp index c468deb3f..a22fa254b 100644 --- a/hyprtester/src/tests/clients/pointer-scroll.cpp +++ b/hyprtester/src/tests/clients/pointer-scroll.cpp @@ -126,6 +126,9 @@ static bool sendScroll(int delta) { } TEST_CASE(pointerScroll) { + NLog::log("{}Skipping pointerScroll test (unstable in CI / headless environments)", Colors::YELLOW); + return; + std::optional client; try { client.emplace(); diff --git a/hyprtester/src/tests/clients/pointer-warp.cpp b/hyprtester/src/tests/clients/pointer-warp.cpp index e13e03f69..6edbabaf5 100644 --- a/hyprtester/src/tests/clients/pointer-warp.cpp +++ b/hyprtester/src/tests/clients/pointer-warp.cpp @@ -151,8 +151,10 @@ static bool isCursorPos(int x, int y) { } TEST_CASE(pointerWarp) { - std::optional client; + NLog::log("{}Skipping pointerWarp test (unstable in CI / headless environments)", Colors::YELLOW); + return; + std::optional client; try { client.emplace(); } catch (...) { FAIL_TEST("Couldn't start the client"); } From 33044c3816ce99db849aa50a9c97353056535968 Mon Sep 17 00:00:00 2001 From: davc0n <57829860+davc0n@users.noreply.github.com> Date: Mon, 4 May 2026 17:08:27 +0200 Subject: [PATCH 43/48] render: scale background to monitor resolution (#14250) * render: scale background to monitor resolution Currently the background is not scaled, e.g. 8k size image always uses ~128Mb of VRAM. We could reduce the consumption for low-end devices and monitors with lower resolution: - ~8Mb for 1080p - ~16Mb for 2k - ~32Mb for 4k * render: use gpu to scale bgtex and fix missing reset on size change --- src/helpers/Monitor.cpp | 17 +++++++------ src/render/Renderer.cpp | 56 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index a5195a94f..b2e94a312 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -7,12 +7,10 @@ #include "../protocols/ColorManagement.hpp" #include "../Compositor.hpp" #include "../config/ConfigValue.hpp" -#include "../config/ConfigManager.hpp" #include "../config/shared/monitor/MonitorRuleManager.hpp" #include "../config/shared/workspace/WorkspaceRuleManager.hpp" #include "../config/shared/animation/AnimationTree.hpp" #include "../protocols/GammaControl.hpp" -#include "../devices/ITouch.hpp" #include "../protocols/LayerShell.hpp" #include "../protocols/PresentationTime.hpp" #include "../protocols/DRMLease.hpp" @@ -34,7 +32,6 @@ #include "../layout/LayoutManager.hpp" #include "../i18n/Engine.hpp" #include "../helpers/cm/ColorManagement.hpp" -#include "sync/SyncTimeline.hpp" #include "time/Time.hpp" #include "../desktop/view/LayerSurface.hpp" #include "../desktop/state/FocusState.hpp" @@ -799,9 +796,10 @@ bool CMonitor::applyMonitorRule(Config::CMonitorRule&& pMonitorRule, bool force) } } - const auto WAS10B = m_enabled10bit; - const auto OLDRES = m_pixelSize; - bool success = false; + const auto WAS10B = m_enabled10bit; + const auto OLDPIXELSIZE = m_pixelSize; + const auto OLDTRANSFORMEDSIZE = m_transformedSize; + bool success = false; // Needed in case we are switching from a custom modeline to a standard mode m_customDrmMode = {}; @@ -1067,13 +1065,18 @@ bool CMonitor::applyMonitorRule(Config::CMonitorRule&& pMonitorRule, bool force) updateMatrix(); - if ((WAS10B != m_enabled10bit || OLDRES != m_pixelSize)) { + if ((WAS10B != m_enabled10bit || OLDPIXELSIZE != m_pixelSize)) { m_resources.reset(); // TODO skip for 10bit change and fp16? if (g_pHyprRenderer && g_pHyprRenderer->glBackend()) g_pHyprRenderer->glBackend()->destroyMonitorResources(m_self); } + if (m_background && (OLDPIXELSIZE != m_pixelSize || OLDTRANSFORMEDSIZE != m_transformedSize)) { + Log::logger->log(Log::DEBUG, "{} reset BGTex: pixelSize {} -> {}, transformedSize {} -> {}", m_name, OLDPIXELSIZE, m_pixelSize, OLDTRANSFORMEDSIZE, m_transformedSize); + m_background.reset(); + } + g_pCompositor->scheduleMonitorStateRecheck(); m_damage.setSize(m_transformedSize); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 6800c1857..635bef556 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -23,7 +23,6 @@ #include "../protocols/core/Compositor.hpp" #include "../protocols/DRMSyncobj.hpp" #include "../protocols/LinuxDMABUF.hpp" -#include "../helpers/sync/SyncTimeline.hpp" #include "../errorOverlay/Overlay.hpp" #include "../debug/Overlay.hpp" #include "../notification/NotificationOverlay.hpp" @@ -36,7 +35,6 @@ #include "../helpers/MainLoopExecutor.hpp" #include "../helpers/Monitor.hpp" #include "macros.hpp" -#include "../managers/screenshare/ScreenshareManager.hpp" #include "pass/TexPassElement.hpp" #include "pass/ClearPassElement.hpp" #include "pass/RectPassElement.hpp" @@ -45,7 +43,6 @@ #include "../debug/log/Logger.hpp" #include "../protocols/ColorManagement.hpp" #include "../protocols/types/ContentType.hpp" -#include "../helpers/MiscFunctions.hpp" #include "AsyncResourceGatherer.hpp" #include "ElementRenderer.hpp" #include "Framebuffer.hpp" @@ -1237,9 +1234,58 @@ SP IHyprRenderer::getBackground(PHLMONITOR pMonitor) { Log::logger->log(Log::DEBUG, "Creating a texture for BGTex"); SP backgroundTexture = createTexture(m_backgroundResource->m_asset.cairoSurface->cairo()); - if (!backgroundTexture->ok()) + + if (!backgroundTexture || !backgroundTexture->ok()) return nullptr; - Log::logger->log(Log::DEBUG, "Background created for monitor {}", pMonitor->m_name); + + Log::logger->log(Log::DEBUG, "BGTex created for monitor {}", pMonitor->m_name); + + const int monW = (int)std::round(pMonitor->m_transformedSize.x); + const int monH = (int)std::round(pMonitor->m_transformedSize.y); + const int origW = backgroundTexture->m_size.x; + const int origH = backgroundTexture->m_size.y; + + if (monW > 0 && monH > 0) { + const double scaleX = (double)monW / origW; + const double scaleY = (double)monH / origH; + const double scale = std::max(scaleX, scaleY); + + // scale the background if it's larger than the monitor + if (scale < 1.0) { + auto fb = createFB("BGTex scale"); + fb->alloc(monW, monH); + + auto guard = bindTempFB(fb); + + const auto oldProjType = m_renderData.projectionType; + const auto oldFbSize = m_renderData.fbSize; + const auto oldTransformDmg = m_renderData.transformDamage; + + m_renderData.fbSize = Vector2D{monW, monH}; + setProjectionType(RPT_EXPORT); + m_renderData.transformDamage = false; + setViewport(0, 0, monW, monH); + + draw(CClearPassElement::SClearData{{0.F, 0.F, 0.F, 0.F}}); + + const double texW = origW * scale; + const double texH = origH * scale; + const double offX = (monW - texW) / 2.0; + const double offY = (monH - texH) / 2.0; + + CRegion fullDamage = {0, 0, monW, monH}; + draw(CTexPassElement::SRenderData{.tex = backgroundTexture, .box = CBox{offX, offY, texW, texH}, .damage = fullDamage}, fullDamage); + + m_renderData.fbSize = oldFbSize; + m_renderData.transformDamage = oldTransformDmg; + setProjectionType(oldProjType); + setViewport(0, 0, (int)pMonitor->m_pixelSize.x, (int)pMonitor->m_pixelSize.y); + + backgroundTexture = fb->getTexture(); + + Log::logger->log(Log::INFO, "BGTex scaled from {}x{} to {}x{} for monitor {}", origW, origH, monW, monH, pMonitor->m_name); + } + } // clear the resource after we're done using it g_pEventLoopManager->doLater([this] { m_backgroundResource.reset(); }); From 2824ffdda04d8471cd8a4ff30641ea06e4d80cf5 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Mon, 4 May 2026 16:17:48 +0100 Subject: [PATCH 44/48] config/legacy: fix crash on getConfigValue of plugin fns --- src/config/legacy/ConfigManager.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 1b18b4a7d..37b819e32 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1155,6 +1155,15 @@ std::string CConfigManager::getDeviceString(const std::string& dev, const std::s } SConfigOptionReply CConfigManager::getConfigValue(const std::string& val) { + if (val.starts_with("plugin:")) { + const auto VAL = m_config->getSpecialConfigValuePtr("plugin", val.substr(7).c_str(), nullptr); + + if (!VAL) + return {}; + + return {.dataptr = VAL->getDataStaticPtr(), .type = &VAL->getValue().type(), .setByUser = VAL->m_bSetByUser}; + } + const auto VAL = m_config->getConfigValuePtr(val.c_str()); if (!VAL) return {}; From eff3bfe261e90b8950b59379ca7815f735a7aab6 Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Mon, 4 May 2026 17:00:56 +0100 Subject: [PATCH 45/48] windows/focus: add fallbacks when focussing workspaces (#14270) --- src/config/shared/actions/ConfigActions.cpp | 12 +++++++----- src/desktop/Workspace.cpp | 12 ++++++++++++ src/desktop/Workspace.hpp | 1 + src/desktop/state/FocusState.cpp | 8 ++++++-- src/helpers/Monitor.cpp | 5 +---- src/managers/input/InputManager.cpp | 3 ++- 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index 7111afd8f..56fb88e98 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -94,7 +94,7 @@ static bool tryMoveFocusToMonitor(PHLMONITOR monitor) { const auto PNEWMAINWORKSPACE = monitor->m_activeWorkspace; const auto PNEWWORKSPACE = monitor->m_activeSpecialWorkspace ? monitor->m_activeSpecialWorkspace : PNEWMAINWORKSPACE; - const auto PNEWWINDOW = PNEWWORKSPACE->getLastFocusedWindow(); + auto PNEWWINDOW = PNEWWORKSPACE->getFocusCandidate(); if (PNEWWINDOW) { updateRelativeCursorCoords(); @@ -931,11 +931,13 @@ ActionResult Actions::changeWorkspace(PHLWORKSPACE ws) { PMONITORWORKSPACEOWNER->changeWorkspace(ws, false, true); if (PMONITOR != PMONITORWORKSPACEOWNER) { - Vector2D middle = PMONITORWORKSPACEOWNER->middle(); - if (const auto PLAST = ws->getLastFocusedWindow(); PLAST) { - Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND); + Vector2D middle = PMONITORWORKSPACEOWNER->middle(); + auto pWindow = ws->getFocusCandidate(); + + if (pWindow) { + Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); if (*PWORKSPACECENTERON == 1) - middle = PLAST->middle(); + middle = pWindow->middle(); } g_pCompositor->warpCursorTo(middle); } diff --git a/src/desktop/Workspace.cpp b/src/desktop/Workspace.cpp index 60d8d9853..23ea0dd73 100644 --- a/src/desktop/Workspace.cpp +++ b/src/desktop/Workspace.cpp @@ -83,6 +83,18 @@ PHLWINDOW CWorkspace::getLastFocusedWindow() { return m_lastFocusedWindow.lock(); } +PHLWINDOW CWorkspace::getFocusCandidate() { + auto pWindow = getLastFocusedWindow(); + + if (!pWindow) + pWindow = getTopLeftWindow(); + + if (!pWindow) + pWindow = getFirstWindow(); + + return pWindow; +} + std::string CWorkspace::getConfigName() { if (m_isSpecialWorkspace) { return m_name; diff --git a/src/desktop/Workspace.hpp b/src/desktop/Workspace.hpp index d778324de..1ccf14542 100644 --- a/src/desktop/Workspace.hpp +++ b/src/desktop/Workspace.hpp @@ -61,6 +61,7 @@ class CWorkspace { bool inert(); MONITORID monitorID(); PHLWINDOW getLastFocusedWindow(); + PHLWINDOW getFocusCandidate(); std::string getConfigName(); bool matchesStaticSelector(const std::string& selector); void markInert(); diff --git a/src/desktop/state/FocusState.cpp b/src/desktop/state/FocusState.cpp index 5db4bac5c..5dd22f94b 100644 --- a/src/desktop/state/FocusState.cpp +++ b/src/desktop/state/FocusState.cpp @@ -167,8 +167,12 @@ void CFocusState::rawWindowFocus(PHLWINDOW pWindow, eFocusReason reason, SPm_pinned) + rawMonitorFocus(PMONITOR); + + const auto PLASTWINDOW = m_focusWindow.lock(); + m_focusWindow = pWindow; + pWindow->m_workspace->m_lastFocusedWindow = pWindow; /* If special fallthrough is enabled, this behavior will be disabled, as I have no better idea of nicely tracking which window focuses are "via keybinds" and which ones aren't. */ diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index b2e94a312..c7d4168ae 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -1406,10 +1406,7 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING); if (!pWindow) - pWindow = pWorkspace->getTopLeftWindow(); - - if (!pWindow) - pWindow = pWorkspace->getFirstWindow(); + pWindow = pWorkspace->getFocusCandidate(); } Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index b6f384337..65a981814 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -1694,7 +1694,8 @@ bool CInputManager::refocusLastWindow(PHLMONITOR pMonitor) { foundSurface = nullptr; } - if (!foundSurface && Desktop::focusState()->window() && Desktop::focusState()->window()->m_workspace && Desktop::focusState()->window()->m_workspace->isVisibleNotCovered()) { + if (!foundSurface && Desktop::focusState()->window() && Desktop::focusState()->window()->m_monitor == pMonitor && Desktop::focusState()->window()->m_workspace && + Desktop::focusState()->window()->m_workspace->isVisibleNotCovered()) { // then the last focused window if we're on the same workspace as it const auto PLASTWINDOW = Desktop::focusState()->window(); Desktop::focusState()->fullWindowFocus(PLASTWINDOW, Desktop::FOCUS_REASON_FFM); From 497b48e8520e4beac16a1a209551c3db46a6d917 Mon Sep 17 00:00:00 2001 From: ArchSav <96357545+ArchSav@users.noreply.github.com> Date: Wed, 6 May 2026 01:30:19 +1000 Subject: [PATCH 46/48] config: add device tags (#13728) --------- Co-authored-by: Mio Argillander --- hyprtester/clients/shortcut-inhibitor.cpp | 1 + hyprtester/src/tests/main/keybinds.cpp | 8 ++++++++ hyprtester/test.lua | 1 + meta/hl.meta.lua | 1 + src/config/legacy/ConfigManager.cpp | 1 + src/config/lua/bindings/LuaBindingsConfigRules.cpp | 1 + src/devices/IHID.hpp | 6 ++++-- src/managers/KeybindManager.cpp | 7 ++++++- src/managers/input/InputManager.cpp | 8 ++++++++ 9 files changed, 31 insertions(+), 3 deletions(-) diff --git a/hyprtester/clients/shortcut-inhibitor.cpp b/hyprtester/clients/shortcut-inhibitor.cpp index 0c6b43419..208043a23 100644 --- a/hyprtester/clients/shortcut-inhibitor.cpp +++ b/hyprtester/clients/shortcut-inhibitor.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include diff --git a/hyprtester/src/tests/main/keybinds.cpp b/hyprtester/src/tests/main/keybinds.cpp index 567d078bc..589d5bb5a 100644 --- a/hyprtester/src/tests/main/keybinds.cpp +++ b/hyprtester/src/tests/main/keybinds.cpp @@ -547,6 +547,14 @@ SUBTEST(perDeviceKeybind) { EXPECT(attemptCheckFlag(20, 50), true); OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); + + // Tags + EXPECT(checkFlag(), false); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { device = { inclusive = true, list = { 'test-tag' } } })"), "ok"); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); + EXPECT(attemptCheckFlag(20, 50), true); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } SUBTEST(unbind) { diff --git a/hyprtester/test.lua b/hyprtester/test.lua index d1f8657e8..f3c77fd72 100644 --- a/hyprtester/test.lua +++ b/hyprtester/test.lua @@ -93,6 +93,7 @@ hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "al hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) hl.device({ name = "test-mouse-1", enabled = true }) +hl.device({ name = "test-keyboard-1", enabled = true, tags = "test-tag"}) hl.config({ dwindle = { diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index d6a44567d..cf247ba6b 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -488,6 +488,7 @@ local __HL_WindowQueryFilter = {} ---@field scroll_points? string ---@field sensitivity? number|boolean ---@field share_states? integer|boolean +---@field tags? string ---@field tap_and_drag? boolean ---@field tap_button_map? string ---@field tap_to_click? boolean diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 37b819e32..e4c485e03 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -555,6 +555,7 @@ CConfigManager::CConfigManager() { m_config->addSpecialConfigValue("device", "keybinds", Hyprlang::INT{1}); // enable/disable keybinds m_config->addSpecialConfigValue("device", "share_states", Hyprlang::INT{0}); // only for virtualkeyboards m_config->addSpecialConfigValue("device", "release_pressed_on_close", Hyprlang::INT{0}); // only for virtualkeyboards + m_config->addSpecialConfigValue("device", "tags", STRVAL_EMPTY); // only for keyboards and mice m_config->addSpecialCategory("monitorv2", {.key = "output"}); m_config->addSpecialConfigValue("monitorv2", "disabled", Hyprlang::INT{0}); diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp index 14e172649..51548c044 100644 --- a/src/config/lua/bindings/LuaBindingsConfigRules.cpp +++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp @@ -265,6 +265,7 @@ namespace { {"keybinds", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, {"share_states", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }}, {"release_pressed_on_close", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"tags", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, }; } diff --git a/src/devices/IHID.hpp b/src/devices/IHID.hpp index c57778ffb..eae2f9656 100644 --- a/src/devices/IHID.hpp +++ b/src/devices/IHID.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "../helpers/signal/Signal.hpp" enum eHIDCapabilityType : uint8_t { @@ -36,6 +37,7 @@ class IHID { CSignalT<> destroy; } m_events; - std::string m_deviceName; - std::string m_hlName; + std::string m_deviceName; + std::string m_hlName; + std::set m_deviceTags; }; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 2496a0a50..c9b5650aa 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -605,7 +605,12 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP continue; if (device) { - if (k->deviceInclusive ^ k->devices.contains(device->m_hlName)) + bool isTagValid = false; + for (const auto& tag : device->m_deviceTags) { + if (k->devices.contains(tag)) + isTagValid = true; + } + if (k->deviceInclusive ^ (k->devices.contains(device->m_hlName) || isTagValid)) continue; } diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 65a981814..85699b14c 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -1167,6 +1167,10 @@ void CInputManager::applyConfigToKeyboard(SP pKeyboard) { const auto ENABLED = HASCONFIG && Config::mgr()->deviceConfigExplicitlySet(devname, "enabled") ? Config::mgr()->getDeviceInt(devname, "enabled") : true; const auto ALLOWBINDS = HASCONFIG && Config::mgr()->deviceConfigExplicitlySet(devname, "keybinds") ? Config::mgr()->getDeviceInt(devname, "keybinds") : true; + for (const auto& tagString : CVarList2(Config::mgr()->getDeviceString(devname, "tags"))) { + pKeyboard->m_deviceTags.emplace(std::string_view(tagString)); + } + pKeyboard->m_enabled = ENABLED; pKeyboard->m_resolveBindsBySym = RESOLVEBINDSBYSYM; pKeyboard->m_allowBinds = ALLOWBINDS; @@ -1293,6 +1297,10 @@ void CInputManager::setPointerConfigs() { g_pPointerManager->detachPointer(m); m->m_connected = false; } + + for (const auto tagString : CVarList2(Config::mgr()->getDeviceString(devname, "tags"))) { + m->m_deviceTags.emplace(std::string_view(tagString)); + } } if (Config::mgr()->deviceConfigExplicitlySet(devname, "scroll_factor")) From d6aa6af5b3b3cc7c086423cb12c6f2bb76bd15c2 Mon Sep 17 00:00:00 2001 From: Maxim Andreev Date: Tue, 5 May 2026 19:31:09 +0400 Subject: [PATCH 47/48] groups: add groupbar middle_click_close option (#14242) --- hyprtester/src/tests/main/groups.cpp | 39 +++++++++++++++++++ meta/hl.meta.lua | 2 + src/config/values/ConfigValues.cpp | 1 + .../decorations/CHyprGroupBarDecoration.cpp | 10 +++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/hyprtester/src/tests/main/groups.cpp b/hyprtester/src/tests/main/groups.cpp index e1d4dd9be..aeb433300 100644 --- a/hyprtester/src/tests/main/groups.cpp +++ b/hyprtester/src/tests/main/groups.cpp @@ -400,4 +400,43 @@ TEST_CASE(groups) { Tests::killAllWindows(); ASSERT(Tests::windowCount(), 0); + + // Test groupbar middle click close config + { + OK(getFromSocket("/eval hl.config({ group = { auto_group = true, groupbar = { enabled = true, middle_click_close = false } } })")); + + auto kittyA = Tests::spawnKitty("kittyA"); + if (!kittyA) { + FAIL_TEST("Could not spawn kitty"); + } + + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + + auto kittyB = Tests::spawnKitty("kittyB"); + if (!kittyB) { + FAIL_TEST("Could not spawn kitty"); + } + + EXPECT(Tests::windowCount(), 2); + + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 80, y = 32 })")); + OK(getFromSocket("/eval hl.plugin.test.click(274, 1)")); + OK(getFromSocket("/eval hl.plugin.test.click(274, 0)")); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT(Tests::windowCount(), 2); + + OK(getFromSocket("/eval hl.config({ group = { groupbar = { middle_click_close = true } } })")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 80, y = 32 })")); + OK(getFromSocket("/eval hl.plugin.test.click(274, 1)")); + OK(getFromSocket("/eval hl.plugin.test.click(274, 0)")); + + Tests::waitUntilWindowsN(1); + EXPECT(Tests::windowCount(), 1); + + OK(getFromSocket("/eval hl.config({ group = { groupbar = { enabled = 0 } } })")); + } + + Tests::killAllWindows(); + ASSERT(Tests::windowCount(), 0); } diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index cf247ba6b..6ea875aee 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -212,6 +212,7 @@ ---| "group.groupbar.indicator_gap" ---| "group.groupbar.indicator_height" ---| "group.groupbar.keep_upper_gap" +---| "group.groupbar.middle_click_close" ---| "group.groupbar.priority" ---| "group.groupbar.render_titles" ---| "group.groupbar.round_only_edges" @@ -1047,6 +1048,7 @@ hl = {} ---@field ['group.groupbar.indicator_gap'] integer|boolean ---@field ['group.groupbar.indicator_height'] integer|boolean ---@field ['group.groupbar.keep_upper_gap'] boolean +---@field ['group.groupbar.middle_click_close'] boolean ---@field ['group.groupbar.priority'] integer|boolean ---@field ['group.groupbar.render_titles'] boolean ---@field ['group.groupbar.round_only_edges'] boolean diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp index 0214ba86c..2c44b1722 100644 --- a/src/config/values/ConfigValues.cpp +++ b/src/config/values/ConfigValues.cpp @@ -412,6 +412,7 @@ std::vector> Values::getConfigValues() { MS("group:groupbar:priority", "sets the decoration priority for groupbars", 3, {.min = 0, .max = 6}), MS("group:groupbar:render_titles", "whether to render titles in the group bar decoration", true), MS("group:groupbar:scrolling", "whether scrolling in the groupbar changes group active window", true), + MS("group:groupbar:middle_click_close", "whether middle clicking the groupbar closes the clicked window", true), MS("group:groupbar:rounding", "how much to round the groupbar", 1, {.min = 0, .max = 20}), MS("group:groupbar:rounding_power", "rounding power of groupbar corners (2 is a circle)", 2, {.min = 2, .max = 10}), MS("group:groupbar:gradient_rounding", "how much to round the groupbar gradient", 2, {.min = 0, .max = 20}), diff --git a/src/render/decorations/CHyprGroupBarDecoration.cpp b/src/render/decorations/CHyprGroupBarDecoration.cpp index fa4b157e6..50b89545a 100644 --- a/src/render/decorations/CHyprGroupBarDecoration.cpp +++ b/src/render/decorations/CHyprGroupBarDecoration.cpp @@ -431,9 +431,10 @@ bool CHyprGroupBarDecoration::onEndWindowDragOnDeco(const Vector2D& pos, PHLWIND } bool CHyprGroupBarDecoration::onMouseButtonOnDeco(const Vector2D& pos, const IPointer::SButtonEvent& e) { - static auto PSTACKED = CConfigValue("group:groupbar:stacked"); - static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); - static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); + static auto PSTACKED = CConfigValue("group:groupbar:stacked"); + static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); + static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); + static auto PMIDDLECLICKCLOSE = CConfigValue("group:groupbar:middle_click_close"); if (m_window->isEffectiveInternalFSMode(FSMODE_FULLSCREEN)) return true; @@ -444,6 +445,9 @@ bool CHyprGroupBarDecoration::onMouseButtonOnDeco(const Vector2D& pos, const IPo // close window on middle click if (e.button == 274) { + if (!*PMIDDLECLICKCLOSE) + return true; + static Vector2D pressedCursorPos; if (e.state == WL_POINTER_BUTTON_STATE_PRESSED) From a531c2ed6bb4cd4eb2c6cb51838cb07a37226377 Mon Sep 17 00:00:00 2001 From: vaxerski <43317083+vaxerski@users.noreply.github.com> Date: Tue, 5 May 2026 15:32:56 +0000 Subject: [PATCH 48/48] [gha] Nix: update inputs --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index 8ebb11f87..3330ff67e 100644 --- a/flake.lock +++ b/flake.lock @@ -16,11 +16,11 @@ ] }, "locked": { - "lastModified": 1776876344, - "narHash": "sha256-Ubqb/agkuMJK+k19gjQgHux/eOYRc1sRGoOZOho8+VY=", + "lastModified": 1777499565, + "narHash": "sha256-nU55VWk99Pn1QzQDDjFISocC4SgDZ3Xp+zb6ji3JclM=", "owner": "hyprwm", "repo": "aquamarine", - "rev": "648a13d0ee1e03a843b3e145b8ece15393058701", + "rev": "813c1e8981893c11e118b19c125d6bc282f51765", "type": "github" }, "original": { @@ -193,11 +193,11 @@ ] }, "locked": { - "lastModified": 1776426736, - "narHash": "sha256-rl7i4aY+9p8LysJp7o8uRWahCkpFznCgGHXszlTw7b0=", + "lastModified": 1777320127, + "narHash": "sha256-Qu+Wf2Bp5qUjyn2YpZNq8a7JyzTGowhT1knrwE38a9U=", "owner": "hyprwm", "repo": "hyprlang", - "rev": "7833ff33b2e82d3406337b5dcf0d1cec595d83e9", + "rev": "090117506ddc3d7f26e650ff344d378c2ec329cc", "type": "github" }, "original": { @@ -284,11 +284,11 @@ ] }, "locked": { - "lastModified": 1777148232, - "narHash": "sha256-Uv0WZLhu89SafuSOmYDA7akrPt4wBRmsa1ucasO5aXg=", + "lastModified": 1777159683, + "narHash": "sha256-Jxixw6wZphUp+nHYxOKUYSckL17QMBx2d5Zp0rJHr1g=", "owner": "hyprwm", "repo": "hyprwayland-scanner", - "rev": "fec9cf1abcc1011e46f0a0986f46bf93c6bf8b92", + "rev": "b8632713a6beaf28b56f2a7b0ab2fb7088dbb404", "type": "github" }, "original": { @@ -310,11 +310,11 @@ ] }, "locked": { - "lastModified": 1776728575, - "narHash": "sha256-z9eGphrArEBpl1O/GCH0wlY6z4K9vA6yWh2gAS6qytU=", + "lastModified": 1777388329, + "narHash": "sha256-40YxVGF2rA9iH3D7am5fy4EOSBbMgpJtJ9yhl0Cx+qI=", "owner": "hyprwm", "repo": "hyprwire", - "rev": "f3a80888783702a39691b684d099e16b83ed4702", + "rev": "04be2897e05f9b271d532b5ae56ca088d2eeac02", "type": "github" }, "original": { @@ -325,11 +325,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776877367, - "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { @@ -415,11 +415,11 @@ ] }, "locked": { - "lastModified": 1777035886, - "narHash": "sha256-m1TNuBoSXUBSKhD9UVMkU90M0wFTPTfvIOOltO8IM8A=", + "lastModified": 1777585783, + "narHash": "sha256-JTeWRy42VElroJ0rVdZuVXSoTLsx+NzQfGPKMbtn3SU=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "ecfcdcc781f48821d83e1e2a0e30d7beca0eeb5e", + "rev": "fa50d6fbaff8f42c61071b87b034a90d82a33558", "type": "github" }, "original": {