Merge branch 'main' into background-scale

This commit is contained in:
davc0n 2026-05-04 14:23:48 +02:00
commit 6b3aafe46e
45 changed files with 800 additions and 191 deletions

View file

@ -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

View file

@ -2,11 +2,13 @@
#include <src/includes.hpp>
#include <sstream>
#include <any>
#include <cmath>
#define private public
#include <src/managers/input/InputManager.hpp>
#include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp>
#include <src/helpers/Monitor.hpp>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
@ -17,6 +19,7 @@
#undef private
#include <hyprutils/utils/ScopeGuard.hpp>
#include <hyprutils/string/Numeric.hpp>
#include <hyprutils/string/VarList.hpp>
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<uint32_t>(data[0]); n)
fingers = n.value();
else
return {.success = false, .error = "invalid input"};
if (const auto n = strToNumber<double>(data[1]); n)
scale = n.value();
else
return {.success = false, .error = "invalid input"};
if (data.size() > 2) {
if (const auto n = strToNumber<double>(data[2]); n)
delta.x = n.value();
else
return {.success = false, .error = "invalid input"};
}
if (data.size() > 3) {
if (const auto n = strToNumber<double>(data[3]); n)
delta.y = n.value();
else
return {.success = false, .error = "invalid input"};
}
if (data.size() > 4) {
if (const auto n = strToNumber<double>(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<float>(data[0]); n)
expected = n.value();
else
return {.success = false, .error = "invalid input"};
if (data.size() > 1) {
if (const auto n = strToNumber<float>(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);

View file

@ -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<CClient> client;
try {
client.emplace();

View file

@ -151,8 +151,10 @@ static bool isCursorPos(int x, int y) {
}
TEST_CASE(pointerWarp) {
std::optional<CClient> client;
NLog::log("{}Skipping pointerWarp test (unstable in CI / headless environments)", Colors::YELLOW);
return;
std::optional<CClient> client;
try {
client.emplace();
} catch (...) { FAIL_TEST("Couldn't start the client"); }

View file

@ -6,12 +6,14 @@
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/string/Numeric.hpp>
#include <csignal>
#include <cerrno>
#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<int>(xSv);
const auto y = strToNumber<int>(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())));
}
}

View file

@ -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 } })"));

View file

@ -1,6 +1,7 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <algorithm>
#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<std::string> 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");
}
}
}

View file

@ -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 })

View file

@ -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()):

View file

@ -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<string, string|number|boolean>): 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,49 @@ 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 clear_tags 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

View file

@ -673,6 +673,9 @@ void CCompositor::initManagers(eManagersInitStage stage) {
Log::logger->log(Log::DEBUG, "Creating the SeatManager!");
g_pSeatManager = makeUnique<CSeatManager>();
Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!");
g_pSessionLockManager = makeUnique<CSessionLockManager>();
// 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<CHyprXWaylandManager>();
Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!");
g_pSessionLockManager = makeUnique<CSessionLockManager>();
Log::logger->log(Log::DEBUG, "Creating the Debug Overlay!");
Debug::overlay();

View file

@ -1538,15 +1538,15 @@ std::optional<std::string> CConfigManager::handleBind(const std::string& command
else if ((ARGS.size() > sc<size_t>(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc<size_t>(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse))
return "bind: too many args";
std::vector<xkb_keysym_t> KEYSYMS;
std::vector<xkb_keysym_t> MODS;
std::vector<KeybindKey> KEYSYMS;
std::vector<KeybindKey> 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]);

View file

@ -160,7 +160,7 @@ static int safeLuaRequire(lua_State* L) {
WP<CConfigManager> Lua::mgr() {
auto& mgr = Config::mgr();
if (mgr->type() != CONFIG_LUA)
if (!mgr || mgr->type() != CONFIG_LUA)
return nullptr;
return dynamicPointerCast<Lua::CConfigManager>(WP<IConfigManager>(mgr));

View file

@ -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);
}
}

View file

@ -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<SDispatcherRef*>(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<SDispatcherRef*>(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;
}

View file

@ -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;
@ -1167,9 +1177,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 +1197,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());
@ -1214,14 +1224,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 +1245,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);
@ -1244,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);
@ -1255,6 +1270,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);

View file

@ -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);

View file

@ -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);
@ -180,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 <typename T>
SParseError parseTableField(lua_State* L, int tableIdx, const char* field, T& parser) {

View file

@ -9,6 +9,7 @@
#include "../../../devices/IKeyboard.hpp"
#include "../../../managers/eventLoop/EventLoopManager.hpp"
#include <hyprutils/string/Numeric.hpp>
#include <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList.hpp>
@ -46,12 +47,12 @@ static bool isSymSpecial(std::string_view sv) {
}
static std::expected<void, std::string> 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<xkb_keysym_t> keysyms;
std::string lastKeyArg;
uint32_t modMask = 0;
std::vector<std::pair<xkb_keysym_t, xkb_keycode_t>> keysyms;
std::string lastKeyArg;
if (sv == "catchall") {
kb.catchAll = true;
@ -86,6 +87,16 @@ static std::expected<void, std::string> parseKeyString(SKeybind& kb, std::string
continue;
}
if (arg.starts_with("code:") && isNumber(std::string{arg.substr(5)})) {
auto res = strToNumber<uint32_t>(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<void, std::string> parseKeyString(SKeybind& kb, std::string
}
lastKeyArg = arg;
keysyms.emplace_back(sym);
keysyms.emplace_back(sym, 0);
}
kb.modmask = modMask;
@ -121,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);
@ -282,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");

View file

@ -605,6 +605,19 @@ ActionResult Actions::tag(const std::string& tagStr, std::optional<PHLWINDOW> w)
return {};
}
ActionResult Actions::clearTags(std::optional<PHLWINDOW> 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<PHLWINDOW> w) {
auto window = xtract(w);
if (!window)

View file

@ -53,6 +53,7 @@ namespace Config::Actions {
ActionResult move(const Vector2D& pos, bool relative = false, std::optional<PHLWINDOW> window = std::nullopt /* Active */);
ActionResult cycleNext(const bool next, std::optional<bool> onlyTiled, std::optional<bool> onlyFloating, std::optional<PHLWINDOW> window = std::nullopt /* Active */);
ActionResult tag(const std::string& tag, std::optional<PHLWINDOW> window = std::nullopt /* Active */);
ActionResult clearTags(std::optional<PHLWINDOW> w = std::nullopt);
ActionResult pass(std::optional<PHLWINDOW> window = std::nullopt /* Active */);
ActionResult pass(uint32_t modMask, uint32_t key, std::optional<PHLWINDOW> window = std::nullopt /* Active */);
ActionResult sendKeyState(uint32_t modMask, uint32_t key, uint32_t state, std::optional<PHLWINDOW> window = std::nullopt /* Active */);

View file

@ -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<Config::INTEGER* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(Config::BOOL))
return std::format("bool: {}\nset: {}", **rc<Config::BOOL* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(Config::FLOAT))
return std::format("float: {:2f}\nset: {}", **rc<Config::FLOAT* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(Config::VEC2))
@ -1644,9 +1646,19 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re
return std::format("str: {}\nset: {}", **rc<Config::STRING* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(void*))
return std::format("custom type: {}\nset: {}", rc<Config::IComplexConfigValue*>((*rc<Hyprlang::CUSTOMTYPE* const*>(VAL))->getData())->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::IComplexConfigValue))
return std::format("custom type: {}\nset: {}", (*rc<Config::IComplexConfigValue* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CCssGapData))
return std::format("css gap data: {}\nset: {}", (*rc<Config::CCssGapData* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CGradientValueData))
return std::format("gradient data: {}\nset: {}", (*rc<Config::CGradientValueData* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CFontWeightConfigValueData))
return std::format("font weight data: {}\nset: {}", (*rc<Config::CFontWeightConfigValueData* const*>(VAL))->toString(), VAR.setByUser);
} else {
if (TYPE == typeid(Config::INTEGER))
return std::format(R"({{"option": "{}", "int": {}, "set": {} }})", curitem, **rc<Config::INTEGER* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(Config::BOOL))
return std::format(R"({{"option": "{}", "bool": {}, "set": {} }})", curitem, (**rc<Config::BOOL* const*>(VAL)) ? "true" : "false", VAR.setByUser);
else if (TYPE == typeid(Config::FLOAT))
return std::format(R"({{"option": "{}", "float": {:2f}, "set": {} }})", curitem, **rc<Config::FLOAT* const*>(VAL), VAR.setByUser);
else if (TYPE == typeid(Config::VEC2))
@ -1662,6 +1674,15 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re
else if (TYPE == typeid(void*))
return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem,
rc<Config::IComplexConfigValue*>((*rc<Hyprlang::CUSTOMTYPE* const*>(VAL))->getData())->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::IComplexConfigValue))
return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem, (*rc<Config::IComplexConfigValue* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CCssGapData))
return std::format(R"({{"option": "{}", "css": "{}", "set": {} }})", curitem, (*rc<Config::CCssGapData* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CGradientValueData))
return std::format(R"({{"option": "{}", "gradient": "{}", "set": {} }})", curitem, (*rc<Config::CGradientValueData* const*>(VAL))->toString(), VAR.setByUser);
else if (TYPE == typeid(Config::CFontWeightConfigValueData))
return std::format(R"({{"option": "{}", "font_weight": "{}", "set": {} }})", curitem, (*rc<Config::CFontWeightConfigValueData* const*>(VAL))->toString(),
VAR.setByUser);
}
return "invalid type (internal error)";

View file

@ -248,6 +248,7 @@ static std::expected<WindowRuleEffectValue, std::string> 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: {

View file

@ -52,9 +52,9 @@ std::unordered_set<CWindowRuleEffectContainer::storageType> 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<float>(value), Types::PRIORITY_WINDOW_RULE);
m_scrollMouse.second |= rule->getPropertiesMask();

View file

@ -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)

View file

@ -65,12 +65,13 @@ static const std::vector<std::string> 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<eWindowRuleEffect>(std::vector<std::string>{EFFECT_STRINGS}) {
;

View file

@ -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,
};

View file

@ -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);
}

View file

@ -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;
};

View file

@ -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 + "*"; });
}

View file

@ -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;

View file

@ -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 {};
}

View file

@ -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<ITarget> 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<ITarget> 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,7 +783,10 @@ 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);
return {};
}

View file

@ -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) {

View file

@ -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<xkb_keysym_t> keybindKeysyms, const std::set<xkb_keysym_t> 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<KeybindKey>& keybindKeysyms, const std::set<KeybindKey>& 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<xkb_keysym_t> boundKeysNotPressed;
std::set<xkb_keysym_t> 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<xkb_keysym_t> symsKb;
for (const auto& k : keybindKeysyms) {
symsKb.emplace(k);
std::vector<KeybindKey> pressed{pressedKeysyms.begin(), pressedKeysyms.end()};
std::vector<int> boundForPressed(pressed.size(), -1);
const auto tryMatch = [&](auto&& self, const size_t boundIdx, std::vector<uint8_t>& 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<size_t>(boundForPressed[pressedIdx]), seen)) {
boundForPressed[pressedIdx] = static_cast<int>(boundIdx);
return true;
}
}
return false;
};
size_t matches = 0;
for (size_t boundIdx = 0; boundIdx < keybindKeysyms.size(); ++boundIdx) {
std::vector<uint8_t> 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)

View file

@ -25,13 +25,15 @@ struct SSubmap {
}
};
using KeybindKey = std::pair<xkb_keysym_t, xkb_keycode_t>;
struct SKeybind {
std::string key = "";
std::vector<xkb_keysym_t> sMkKeys = {};
std::vector<KeybindKey> sMkKeys = {};
uint32_t keycode = 0;
bool catchAll = false;
uint32_t modmask = 0;
std::vector<xkb_keysym_t> sMkMods = {};
std::vector<KeybindKey> 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<IKeyboard>, SP<IHID>);
std::set<xkb_keysym_t> m_mkKeys = {};
std::set<xkb_keysym_t> m_mkMods = {};
std::set<KeybindKey> m_mkKeys = {};
std::set<KeybindKey> m_mkMods = {};
eMultiKeyCase mkBindMatches(const SP<SKeybind>);
eMultiKeyCase mkKeysymSetMatches(const std::vector<xkb_keysym_t>, const std::set<xkb_keysym_t>);
eMultiKeyCase mkKeysymSetMatches(const std::vector<KeybindKey>&, const std::set<KeybindKey>&);
bool handleInternalKeybinds(xkb_keysym_t);
bool handleVT(xkb_keysym_t);

View file

@ -105,8 +105,10 @@ void CSessionLockManager::onNewSessionLock(SP<CSessionLock> pLock) {
return;
}
m_sessionLock->sendDeniedTimer = makeShared<CEventLoopTimer>(
// 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<CEventLoopTimer>(
// 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<CSessionLock> 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;
}

View file

@ -31,7 +31,7 @@ struct SSessionLockSurface {
struct SSessionLock {
WP<CSessionLock> lock;
CTimer lockTimer;
SP<CEventLoopTimer> sendDeniedTimer;
SP<CEventLoopTimer> sendLockedTimer;
std::vector<SP<SSessionLockSurface>> vSessionLockSurfaces;
@ -73,7 +73,7 @@ class CSessionLockManager {
} m_listeners;
void onNewSessionLock(SP<CSessionLock> pWlrLock);
void removeSendDeniedTimer();
void removeSendLockedTimer();
};
inline UP<CSessionLockManager> g_pSessionLockManager;

View file

@ -265,31 +265,56 @@ 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;
if (CONSTRAINT) {
if (CONSTRAINT->isLocked()) {
const auto HINT = CONSTRAINT->logicPositionHint();
g_pCompositor->warpCursorTo(HINT, true);
} 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<uint64_t>(time) * 1000, {}, {});
}
auto confineToRegion = [&](const CRegion& rg, SP<Desktop::View::CWLSurface> surf) {
if (!surf)
return;
} else
Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc<uintptr_t>(SURF.get()),
rc<uintptr_t>(CONSTRAINT.get()));
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<uint64_t>(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 {
Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc<uintptr_t>(SURF.get()),
rc<uintptr_t>(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())

View file

@ -2,19 +2,40 @@
#include "../../../../Compositor.hpp"
#include "../../../../helpers/Monitor.hpp"
#include "../../../../managers/input/InputManager.hpp"
#include <hyprutils/string/Numeric.hpp>
CCursorZoomTrackpadGesture::CCursorZoomTrackpadGesture(const std::string& first, const std::string& second) {
try {
m_zoomValue = std::stof(first);
} catch (...) { ; }
if (const auto n = Hyprutils::String::strToNumber<float>(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<float>(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();
}

View file

@ -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;

View file

@ -140,16 +140,18 @@ WP<CScreenshareSession> CScreenshareManager::getManagedSession(eScreenshareType
m_sessions.emplace_back(session);
it = m_managedSessions.emplace(m_managedSessions.end(), makeUnique<SManagedSession>(std::move(session)));
auto& managed = *it;
managed->stoppedListener = managed->m_session->m_events.stopped.listen([managed = WP<SManagedSession>(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<SManagedSession>(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) {

View file

@ -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() {
@ -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]() {

View file

@ -2191,8 +2191,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);

View file

@ -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) {