diff --git a/example/layouts/columns.lua b/example/layouts/columns.lua new file mode 100644 index 000000000..dc907cd4c --- /dev/null +++ b/example/layouts/columns.lua @@ -0,0 +1,12 @@ +hl.layout.register("columns", { + recalculate = function(ctx) + local n = #ctx.targets + if n == 0 then + return + end + + for i, target in ipairs(ctx.targets) do + target:place(ctx:column(i, n)) + end + end, +}) diff --git a/example/layouts/grid.lua b/example/layouts/grid.lua new file mode 100644 index 000000000..10a0cab12 --- /dev/null +++ b/example/layouts/grid.lua @@ -0,0 +1,14 @@ +hl.layout.register("grid", { + recalculate = function(ctx) + local n = #ctx.targets + if n == 0 then + return + end + + local cols = math.ceil(math.sqrt(n)) + + for i, target in ipairs(ctx.targets) do + target:place(ctx:grid_cell(i, cols)) + end + end, +}) diff --git a/example/layouts/manual.lua b/example/layouts/manual.lua new file mode 100644 index 000000000..1ba2ebd05 --- /dev/null +++ b/example/layouts/manual.lua @@ -0,0 +1,144 @@ +local state = { + order = {}, + split = {}, + default_split = "h", +} + +local function target_id(target) + local window = target.window + return window and tostring(window.stable_id) or tostring(target.index) +end + +local function active_id(ctx) + for _, target in ipairs(ctx.targets) do + local window = target.window + if window and window.active then + return target_id(target) + end + end + + return state.order[#state.order] +end + +local function index_of(tbl, value) + for i, v in ipairs(tbl) do + if v == value then + return i + end + end +end + +local function sync_order(ctx) + local present = {} + local targets = {} + + for _, target in ipairs(ctx.targets) do + local id = target_id(target) + present[id] = true + targets[id] = target + end + + local old_order = state.order + state.order = {} + + for _, id in ipairs(old_order) do + if present[id] then + table.insert(state.order, id) + else + state.split[id] = nil + end + end + + local focused = active_id(ctx) + for _, target in ipairs(ctx.targets) do + local id = target_id(target) + if not index_of(state.order, id) then + local after = focused and index_of(state.order, focused) + table.insert(state.order, after and (after + 1) or (#state.order + 1), id) + end + end + + return targets +end + +local function place_chain(ctx, targets, ids, area, i) + if i > #ids then + return + end + + local target = targets[ids[i]] + if not target then + return + end + + if i == #ids then + target:place(area) + return + end + + local split = state.split[ids[i]] or state.default_split + if split == "v" then + target:place(ctx:split(area, "top", 0.5)) + place_chain(ctx, targets, ids, ctx:split(area, "bottom", 0.5), i + 1) + else + target:place(ctx:split(area, "left", 0.5)) + place_chain(ctx, targets, ids, ctx:split(area, "right", 0.5), i + 1) + end +end + +local function move_active(ctx, delta) + local id = active_id(ctx) + local i = id and index_of(state.order, id) + local j = i and (i + delta) + + if not i or j < 1 or j > #state.order then + return + end + + state.order[i], state.order[j] = state.order[j], state.order[i] +end + +hl.layout.register("manual", { + recalculate = function(ctx) + local targets = sync_order(ctx) + place_chain(ctx, targets, state.order, ctx.area, 1) + end, + + layout_msg = function(ctx, msg) + local id = active_id(ctx) + local command = msg:match("^(%S+)") + + if command == "splith" or command == "h" then + if id then + state.split[id] = "h" + end + elseif command == "splitv" or command == "v" then + if id then + state.split[id] = "v" + end + elseif command == "splittoggle" or command == "toggle" then + if id then + state.split[id] = state.split[id] == "v" and "h" or "v" + end + elseif command == "promote" then + local i = id and index_of(state.order, id) + if i then + table.remove(state.order, i) + table.insert(state.order, 1, id) + end + elseif command == "swapnext" then + move_active(ctx, 1) + elseif command == "swapprev" then + move_active(ctx, -1) + elseif command == "rotate" then + for k, v in pairs(state.split) do + state.split[k] = v == "v" and "h" or "v" + end + state.default_split = state.default_split == "v" and "h" or "v" + else + return "manual: expected splith, splitv, splittoggle, promote, swapnext, swapprev, or rotate" + end + + return true + end, +}) diff --git a/example/layouts/spiral.lua b/example/layouts/spiral.lua new file mode 100644 index 000000000..e97f4dbdd --- /dev/null +++ b/example/layouts/spiral.lua @@ -0,0 +1,55 @@ +local state = { + ratio = 0.58, + offset = 0, +} + +local sides = { "left", "top", "right", "bottom" } +local opposite = { + left = "right", + right = "left", + top = "bottom", + bottom = "top", +} + +local function clamp(x, min, max) + return math.max(min, math.min(max, x)) +end + +hl.layout.register("spiral", { + recalculate = function(ctx) + local n = #ctx.targets + if n == 0 then + return + end + + local area = ctx.area + + for i, target in ipairs(ctx.targets) do + if i == n then + target:place(area) + else + local side = sides[((i - 1 + state.offset) % #sides) + 1] + target:place(ctx:split(area, side, state.ratio)) + area = ctx:split(area, opposite[side], 1.0 - state.ratio) + end + end + end, + + layout_msg = function(ctx, msg) + local command, arg = msg:match("^(%S+)%s*(.*)$") + + if command == "ratio" then + state.ratio = clamp(tonumber(arg) or state.ratio, 0.1, 0.9) + elseif command == "grow" then + state.ratio = clamp(state.ratio + 0.05, 0.1, 0.9) + elseif command == "shrink" then + state.ratio = clamp(state.ratio - 0.05, 0.1, 0.9) + elseif command == "rotate" then + state.offset = (state.offset + 1) % #sides + else + return "spiral: expected ratio <0.1..0.9>, grow, shrink, or rotate" + end + + return true + end, +}) diff --git a/hyprtester/src/tests/main/layout_custom.cpp b/hyprtester/src/tests/main/layout_custom.cpp new file mode 100644 index 000000000..9a98a74a2 --- /dev/null +++ b/hyprtester/src/tests/main/layout_custom.cpp @@ -0,0 +1,56 @@ +#include "../shared.hpp" +#include "../../shared.hpp" +#include "../../hyprctlCompat.hpp" +#include "tests.hpp" + +TEST_CASE(layoutCustomGrid) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'lua:grid' } })")); + + ASSERT(!!Tests::spawnKitty("kitty_A"), true); + ASSERT(!!Tests::spawnKitty("kitty_B"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, "size: 931,1036", 2); + } + + ASSERT(!!Tests::spawnKitty("kitty_C"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, "size: 931,511", 3); + } + + ASSERT(!!Tests::spawnKitty("kitty_D"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, "size: 931,511", 4); + } +} + +TEST_CASE(layoutCustomColumns) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'lua:columns' } })")); + + ASSERT(!!Tests::spawnKitty("kitty_A"), true); + ASSERT(!!Tests::spawnKitty("kitty_B"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, "size: 931,1036", 2); + } + + ASSERT(!!Tests::spawnKitty("kitty_C"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, ",1036\n", 3); // this won't split evenly + } + + ASSERT(!!Tests::spawnKitty("kitty_D"), true); + + { + auto clients = getFromSocket("/clients"); + EXPECT_COUNT_STRING(clients, ",1036\n", 4); // this won't split evenly + } +} diff --git a/hyprtester/test.lua b/hyprtester/test.lua index f3c77fd72..693513aeb 100644 --- a/hyprtester/test.lua +++ b/hyprtester/test.lua @@ -294,3 +294,32 @@ hl.gesture({ fingers = 4, direction = "left", action = function() hl.dispatch(hl hl.gesture({ fingers = 2, direction = "pinch", action = "cursorZoom", zoom_level = "1", mode = "live" }) hl.gesture({ fingers = 2, direction = "right", action = "float", disable_inhibit = true }) + +hl.layout.register("columns", { + recalculate = function(ctx) + local n = #ctx.targets + if n == 0 then + return + end + + for i, target in ipairs(ctx.targets) do + target:place(ctx:column(i, n)) + end + end, +}) + +hl.layout.register("grid", { + recalculate = function(ctx) + local n = #ctx.targets + if n == 0 then + return + end + + local cols = math.ceil(math.sqrt(n)) + + for i, target in ipairs(ctx.targets) do + target:place(ctx:grid_cell(i, cols)) + end + end, +}) + diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py index 7ef70babb..0e94aa6e8 100644 --- a/meta/generateLuaStubs.py +++ b/meta/generateLuaStubs.py @@ -87,18 +87,18 @@ def merge_node(dst: ApiNode, src: ApiNode) -> None: def parse_binding_tree(root: Path) -> tuple[ApiNode, set[str]]: - bindings_dir = root / "src/config/lua/bindings" + lua_dir = root / "src/config/lua" root_node = ApiNode() callable_namespaces: set[str] = set() register_header = re.compile( - r"void\s+Internal::register\w+Bindings\s*\([^)]*\)\s*\{", re.MULTILINE + r"void\s+(?:(?:Config::Lua::Bindings::)?Internal::)?register\w+Bindings\s*\([^)]*\)\s*\{", re.MULTILINE ) - set_fn = re.compile(r'Internal::set(?:Mgr)?Fn\(\s*L\s*,(?:\s*mgr\s*,)?\s*"([^"]+)"\s*,') + set_fn = re.compile(r'(?:Internal::)?set(?:Mgr)?Fn\(\s*L\s*,(?:\s*mgr\s*,)?\s*"([^"]+)"\s*,') set_field = re.compile(r'lua_setfield\(L,\s*-2,\s*"([^"]+)"\s*\);') - for cpp in sorted(bindings_dir.glob("*.cpp")): + for cpp in sorted(lua_dir.rglob("*.cpp")): source = read_text(cpp) for _, body in extract_function_bodies(source, register_header): local_root = ApiNode() @@ -503,6 +503,7 @@ def generate_stub(root: Path) -> str: "hl.get_current_submap": "fun(): string", "hl.notification.create": "fun(opts?: HL.NotificationOptions): HL.Notification", "hl.notification.get": "fun(): HL.Notification[]", + "hl.layout.register": "fun(name: string, provider: HL.LayoutProvider): nil", "hl.exec_cmd": "fun(cmd: string, rules?: table): nil", } api_signatures.update(query_overrides) @@ -539,6 +540,59 @@ def generate_stub(root: Path) -> str: ) lines.append("") + lines.extend( + emit_class_block( + "HL.Box", + [ + ("x", "number", False), + ("y", "number", False), + ("w", "number", False), + ("h", "number", False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.LayoutTarget", + [ + ("index", "integer", False), + ("window", "HL.Window|nil", False), + ("box", "HL.Box", False), + ("place", "fun(self: HL.LayoutTarget, box: HL.Box): nil", False), + ("set_box", "fun(self: HL.LayoutTarget, box: HL.Box): nil", False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.LayoutContext", + [ + ("area", "HL.Box", False), + ("targets", "HL.LayoutTarget[]", False), + ("grid_cell", "fun(self: HL.LayoutContext, i: integer, cols: integer, rows?: integer): HL.Box", False), + ("column", "fun(self: HL.LayoutContext, i: integer, n: integer): HL.Box", False), + ("row", "fun(self: HL.LayoutContext, i: integer, n: integer): HL.Box", False), + ("split", "fun(self: HL.LayoutContext, box: HL.Box, side: 'left'|'right'|'top'|'bottom'|'up'|'down', ratio: number): HL.Box", False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.LayoutProvider", + [ + ("recalculate", "fun(ctx: HL.LayoutContext): nil", False), + ("layout_msg", "fun(ctx: HL.LayoutContext, msg: string): boolean|string|nil", True), + ], + ) + ) + lines.append("") + lines.extend( emit_class_block( "HL.BindOptions", diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index 6ea875aee..6e34ff344 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -387,6 +387,35 @@ local __HL_Dispatcher = {} ---@field y number local __HL_Vec2 = {} +---@class HL.Box +---@field x number +---@field y number +---@field w number +---@field h number +local __HL_Box = {} + +---@class HL.LayoutTarget +---@field index integer +---@field window HL.Window|nil +---@field box HL.Box +---@field place fun(self: HL.LayoutTarget, box: HL.Box): nil +---@field set_box fun(self: HL.LayoutTarget, box: HL.Box): nil +local __HL_LayoutTarget = {} + +---@class HL.LayoutContext +---@field area HL.Box +---@field targets HL.LayoutTarget[] +---@field grid_cell fun(self: HL.LayoutContext, i: integer, cols: integer, rows?: integer): HL.Box +---@field column fun(self: HL.LayoutContext, i: integer, n: integer): HL.Box +---@field row fun(self: HL.LayoutContext, i: integer, n: integer): HL.Box +---@field split fun(self: HL.LayoutContext, box: HL.Box, side: 'left'|'right'|'top'|'bottom'|'up'|'down', ratio: number): HL.Box +local __HL_LayoutContext = {} + +---@class HL.LayoutProvider +---@field recalculate fun(ctx: HL.LayoutContext): nil +---@field layout_msg? fun(ctx: HL.LayoutContext, msg: string): boolean|string|nil +local __HL_LayoutProvider = {} + ---@class HL.BindOptions ---@field repeating? boolean ---@field locked? boolean @@ -783,6 +812,7 @@ local __HL_Workspace = {} ---@field window_rule fun(spec: HL.WindowRuleSpec): HL.WindowRule ---@field workspace_rule fun(spec: HL.WorkspaceRuleSpec): nil ---@field dsp HL.DspNamespace +---@field layout HL.LayoutNamespace ---@field notification HL.NotificationNamespace ---@field plugin HL.PluginNamespace local __HL_API = {} @@ -855,6 +885,10 @@ local __HL_DspWindowNamespace = {} ---@field toggle_special fun(...): HL.Dispatcher local __HL_DspWorkspaceNamespace = {} +---@class HL.LayoutNamespace +---@field register fun(name: string, provider: HL.LayoutProvider): nil +local __HL_LayoutNamespace = {} + ---@class HL.NotificationNamespace ---@field create fun(opts?: HL.NotificationOptions): HL.Notification ---@field get fun(): HL.Notification[] diff --git a/src/config/lua/ConfigManager.cpp b/src/config/lua/ConfigManager.cpp index 3ad70b9ba..0fe75b18b 100644 --- a/src/config/lua/ConfigManager.cpp +++ b/src/config/lua/ConfigManager.cpp @@ -270,6 +270,7 @@ void CConfigManager::reinitLuaState() { m_eventHandler.reset(); cleanTimers(); + clearLuaLayoutProviders(); if (m_lua) { lua_close(m_lua); @@ -420,6 +421,7 @@ void CConfigManager::reload() { Desktop::Rule::ruleEngine()->clearAllRules(); g_pTrackpadGestures->clearGestures(); cleanTimers(); + clearLuaLayoutProviders(); m_luaWindowRules.clear(); m_luaLayerRules.clear(); m_errors.clear(); diff --git a/src/config/lua/ConfigManager.hpp b/src/config/lua/ConfigManager.hpp index 773877583..4e670b232 100644 --- a/src/config/lua/ConfigManager.hpp +++ b/src/config/lua/ConfigManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../../helpers/memory/Memory.hpp" #include "../ConfigManager.hpp" @@ -38,6 +39,10 @@ namespace Config::Lua { class CConfigManagerPluginLuaTestAccessor; } +namespace Config::Lua::Layouts { + struct SLuaLayoutProvider; +} + namespace Config::Lua::Bindings { void registerBindings(lua_State* L, CConfigManager* mgr); } @@ -87,6 +92,7 @@ namespace Config::Lua { void registerLuaRef(int ref); void callLuaFn(int ref); + std::expected registerLuaLayoutProvider(std::string name, lua_State* L, int providerTableIdx); // execute an arbitrary lua string on the current state. std::optional eval(const std::string& code); @@ -100,6 +106,7 @@ namespace Config::Lua { static constexpr int LUA_TIMEOUT_EVENT_CALLBACK_MS = 50; static constexpr int LUA_TIMEOUT_KEYBIND_CALLBACK_MS = 100; static constexpr int LUA_TIMEOUT_TIMER_CALLBACK_MS = 50; + static constexpr int LUA_TIMEOUT_LAYOUT_CALLBACK_MS = 50; static constexpr int LUA_TIMEOUT_EVAL_MS = 250; static constexpr int LUA_TIMEOUT_DISPATCH_MS = 100; @@ -136,34 +143,36 @@ namespace Config::Lua { std::unordered_map> m_luaLayerRules; private: - void reinitLuaState(); - void postConfigReload(); - void registerValue(const char* name, ILuaConfigValue* val); - void cleanTimers(); - void clearHeldLuaRefs(); - std::string luaConfigValueName(const std::string& s); - std::expected registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name); - std::expected unregisterPluginLuaFunctionInState(const std::string& namespace_, const std::string& name); - void erasePluginLuaFunction(uint64_t id); - void reregisterLuaPluginFns(); + void reinitLuaState(); + void postConfigReload(); + void registerValue(const char* name, ILuaConfigValue* val); + void cleanTimers(); + void clearLuaLayoutProviders(); + void clearHeldLuaRefs(); + std::string luaConfigValueName(const std::string& s); + std::expected registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name); + std::expected unregisterPluginLuaFunctionInState(const std::string& namespace_, const std::string& name); + void erasePluginLuaFunction(uint64_t id); + void reregisterLuaPluginFns(); - static void watchdogHook(lua_State* L, lua_Debug* ar); + static void watchdogHook(lua_State* L, lua_Debug* ar); - lua_State* m_lua = nullptr; + lua_State* m_lua = nullptr; - bool m_lastConfigVerificationWasSuccessful = true; - bool m_isFirstLaunch = true; - bool m_manualCrashInitiated = false; - bool m_watchdogActive = false; - bool m_isParsingConfig = false; - bool m_isEvaluating = false; + bool m_lastConfigVerificationWasSuccessful = true; + bool m_isFirstLaunch = true; + bool m_manualCrashInitiated = false; + bool m_watchdogActive = false; + bool m_isParsingConfig = false; + bool m_isEvaluating = false; - std::chrono::steady_clock::time_point m_watchdogDeadline; - std::string m_watchdogContext; + std::chrono::steady_clock::time_point m_watchdogDeadline; + std::string m_watchdogContext; - std::string m_mainConfigPath; + std::string m_mainConfigPath; - std::vector m_heldLuaRefs; + std::vector m_heldLuaRefs; + std::vector> m_luaLayoutProviders; // this is here for legacy reasons. std::unordered_map m_configPtrMap; diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 9b61360cb..40fe804a1 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -202,6 +202,7 @@ namespace Config::Lua::Bindings::Internal { bool hasTableField(lua_State* L, int tableIdx, const char* field); void registerToplevelBindings(lua_State* L, CConfigManager* mgr); + void registerLayoutBindings(lua_State* L, CConfigManager* mgr); void registerQueryBindings(lua_State* L); void registerNotificationBindings(lua_State* L); void registerConfigRuleBindings(lua_State* L, CConfigManager* mgr); diff --git a/src/config/lua/bindings/LuaBindingsRegistration.cpp b/src/config/lua/bindings/LuaBindingsRegistration.cpp index 0cb593649..f59e374bc 100644 --- a/src/config/lua/bindings/LuaBindingsRegistration.cpp +++ b/src/config/lua/bindings/LuaBindingsRegistration.cpp @@ -86,6 +86,7 @@ void Internal::registerBindingsImpl(lua_State* L, CConfigManager* mgr) { Internal::registerConfigRuleBindings(L, mgr); Internal::registerToplevelBindings(L, mgr); + Internal::registerLayoutBindings(L, mgr); Internal::registerQueryBindings(L); Internal::registerDispatcherBindings(L); Internal::registerNotificationBindings(L); diff --git a/src/config/lua/layout/LuaLayoutContext.cpp b/src/config/lua/layout/LuaLayoutContext.cpp new file mode 100644 index 000000000..f7370e1be --- /dev/null +++ b/src/config/lua/layout/LuaLayoutContext.cpp @@ -0,0 +1,144 @@ +#include "LuaLayoutContext.hpp" + +#include "LuaLayoutTarget.hpp" +#include "../bindings/LuaBindingsInternal.hpp" + +#include +#include +#include + +using namespace Config::Lua::Layouts; + +void Config::Lua::Layouts::pushBox(lua_State* L, const CBox& box) { + lua_newtable(L); + lua_pushnumber(L, box.x); + lua_setfield(L, -2, "x"); + lua_pushnumber(L, box.y); + lua_setfield(L, -2, "y"); + lua_pushnumber(L, box.w); + lua_setfield(L, -2, "w"); + lua_pushnumber(L, box.h); + lua_setfield(L, -2, "h"); +} + +bool Config::Lua::Layouts::boxFromTable(lua_State* L, int idx, CBox& box) { + idx = lua_absindex(L, idx); + if (!lua_istable(L, idx)) + return false; + + auto getNum = [&](const char* key, double& out) -> bool { + lua_getfield(L, idx, key); + if (!lua_isnumber(L, -1)) { + lua_pop(L, 1); + return false; + } + out = lua_tonumber(L, -1); + lua_pop(L, 1); + return true; + }; + + return getNum("x", box.x) && getNum("y", box.y) && getNum("w", box.w) && getNum("h", box.h); +} + +static CBox areaFromContext(lua_State* L, int idx) { + idx = lua_absindex(L, idx); + CBox area; + lua_getfield(L, idx, "area"); + boxFromTable(L, -1, area); + lua_pop(L, 1); + return area; +} + +static size_t targetCountFromContext(lua_State* L, int idx) { + idx = lua_absindex(L, idx); + size_t count = 0; + lua_getfield(L, idx, "targets"); + if (lua_istable(L, -1)) + count = lua_rawlen(L, -1); + lua_pop(L, 1); + return count; +} + +static int ctxGridCell(lua_State* L) { + const auto AREA = areaFromContext(L, 1); + const int i = std::max(1, sc(luaL_checkinteger(L, 2))); + const int cols = std::max(1, sc(luaL_checkinteger(L, 3))); + int rows = 0; + + if (lua_gettop(L) >= 4 && lua_isnumber(L, 4)) + rows = std::max(1, sc(lua_tointeger(L, 4))); + else { + const auto count = std::max(1, targetCountFromContext(L, 1)); + rows = std::max(1, sc(std::ceil(sc(count) / sc(cols)))); + } + + const int row = (i - 1) / cols; + const int col = (i - 1) % cols; + + pushBox(L, CBox{AREA.x + AREA.w * col / cols, AREA.y + AREA.h * row / rows, AREA.w / cols, AREA.h / rows}.noNegativeSize()); + return 1; +} + +static int ctxColumn(lua_State* L) { + const auto AREA = areaFromContext(L, 1); + const int i = std::max(1, sc(luaL_checkinteger(L, 2))); + const int n = std::max(1, sc(luaL_checkinteger(L, 3))); + + pushBox(L, CBox{AREA.x + AREA.w * (i - 1) / n, AREA.y, AREA.w / n, AREA.h}.noNegativeSize()); + return 1; +} + +static int ctxRow(lua_State* L) { + const auto AREA = areaFromContext(L, 1); + const int i = std::max(1, sc(luaL_checkinteger(L, 2))); + const int n = std::max(1, sc(luaL_checkinteger(L, 3))); + + pushBox(L, CBox{AREA.x, AREA.y + AREA.h * (i - 1) / n, AREA.w, AREA.h / n}.noNegativeSize()); + return 1; +} + +static int ctxSplit(lua_State* L) { + CBox area; + if (!boxFromTable(L, 2, area)) + return Config::Lua::Bindings::Internal::configError(L, "ctx:split expects a box table as first argument"); + + const std::string_view side = luaL_checkstring(L, 3); + const double ratio = std::clamp(luaL_checknumber(L, 4), 0.0, 1.0); + + if (side == "left") + pushBox(L, CBox{area.x, area.y, area.w * ratio, area.h}.noNegativeSize()); + else if (side == "right") + pushBox(L, CBox{area.x + area.w * (1.0 - ratio), area.y, area.w * ratio, area.h}.noNegativeSize()); + else if (side == "top" || side == "up") + pushBox(L, CBox{area.x, area.y, area.w, area.h * ratio}.noNegativeSize()); + else if (side == "bottom" || side == "down") + pushBox(L, CBox{area.x, area.y + area.h * (1.0 - ratio), area.w, area.h * ratio}.noNegativeSize()); + else + return Config::Lua::Bindings::Internal::configError(L, "ctx:split side must be left, right, top, or bottom"); + + return 1; +} + +void Config::Lua::Layouts::pushLayoutContext(lua_State* L, const std::vector>& targets, const CBox& area) { + lua_newtable(L); + + pushBox(L, area); + lua_setfield(L, -2, "area"); + + lua_newtable(L); + int i = 1; + for (const auto& target : targets) { + pushLayoutTarget(L, target, i); + lua_rawseti(L, -2, i++); + } + lua_setfield(L, -2, "targets"); + + lua_pushcfunction(L, ctxGridCell); + lua_setfield(L, -2, "grid_cell"); + lua_pushcfunction(L, ctxColumn); + lua_setfield(L, -2, "column"); + lua_pushcfunction(L, ctxRow); + lua_setfield(L, -2, "row"); + lua_pushcfunction(L, ctxSplit); + lua_setfield(L, -2, "split"); +} diff --git a/src/config/lua/layout/LuaLayoutContext.hpp b/src/config/lua/layout/LuaLayoutContext.hpp new file mode 100644 index 000000000..2f9970378 --- /dev/null +++ b/src/config/lua/layout/LuaLayoutContext.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "../../../helpers/math/Math.hpp" +#include "../../../helpers/memory/Memory.hpp" + +#include + +extern "C" { +#include +} + +namespace Layout { + class ITarget; +} + +namespace Config::Lua::Layouts { + void pushBox(lua_State* L, const CBox& box); + bool boxFromTable(lua_State* L, int idx, CBox& box); + void pushLayoutContext(lua_State* L, const std::vector>& targets, const CBox& area); +} diff --git a/src/config/lua/layout/LuaLayoutProvider.cpp b/src/config/lua/layout/LuaLayoutProvider.cpp new file mode 100644 index 000000000..344362ff8 --- /dev/null +++ b/src/config/lua/layout/LuaLayoutProvider.cpp @@ -0,0 +1,323 @@ +#include "LuaLayoutProvider.hpp" + +#include "LuaLayoutContext.hpp" +#include "LuaLayoutTarget.hpp" +#include "../ConfigManager.hpp" +#include "../bindings/LuaBindingsInternal.hpp" + +#include "../../../debug/log/Logger.hpp" +#include "../../../layout/algorithm/Algorithm.hpp" +#include "../../../layout/space/Space.hpp" +#include "../../../layout/target/Target.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" + +#include +#include +#include + +using namespace Config::Lua; +using namespace Config::Lua::Layouts; + +static std::string normalizeLuaLayoutName(std::string name) { + if (!name.starts_with("lua:")) + name = "lua:" + name; + return name; +} + +CLuaTiledAlgorithm::CLuaTiledAlgorithm(SP provider) : m_provider(std::move(provider)) { + ; +} + +void CLuaTiledAlgorithm::newTarget(SP target) { + m_targets.emplace_back(target); + recalculate(); +} + +void CLuaTiledAlgorithm::movedTarget(SP target, std::optional focalPoint) { + newTarget(target); +} + +void CLuaTiledAlgorithm::removeTarget(SP target) { + std::erase_if(m_targets, [&target](const auto& t) { return !t || t.lock() == target; }); + recalculate(); +} + +void CLuaTiledAlgorithm::resizeTarget(const Vector2D& Δ, SP target, Layout::eRectCorner corner) { + recalculate(); +} + +void CLuaTiledAlgorithm::recalculate() { + auto targets = liveTargets(); + if (targets.empty()) + return; + + if (!callRecalculate(targets)) + applyDefaultGrid(targets); +} + +void CLuaTiledAlgorithm::swapTargets(SP a, SP b) { + auto ia = std::ranges::find_if(m_targets, [&a](const auto& t) { return t.lock() == a; }); + auto ib = std::ranges::find_if(m_targets, [&b](const auto& t) { return t.lock() == b; }); + + if (ia != m_targets.end() && ib != m_targets.end()) + std::iter_swap(ia, ib); + else { + if (ia != m_targets.end()) + *ia = b; + if (ib != m_targets.end()) + *ib = a; + } + + recalculate(); +} + +void CLuaTiledAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { + auto it = std::ranges::find_if(m_targets, [&t](const auto& target) { return target.lock() == t; }); + if (it == m_targets.end()) + return; + + if ((dir == Math::DIRECTION_LEFT || dir == Math::DIRECTION_UP) && it != m_targets.begin()) + std::iter_swap(it, std::prev(it)); + else if ((dir == Math::DIRECTION_RIGHT || dir == Math::DIRECTION_DOWN) && std::next(it) != m_targets.end()) + std::iter_swap(it, std::next(it)); + else + return; + + recalculate(); +} + +Config::ErrorResult CLuaTiledAlgorithm::layoutMsg(const std::string_view& sv) { + if (!m_provider || !m_provider->active || !m_provider->state) + return {}; + + auto targets = liveTargets(); + auto parent = m_parent.lock(); + auto space = parent ? parent->space() : nullptr; + if (!space) + return {}; + + lua_State* L = m_provider->state; + const int top = lua_gettop(L); + + lua_rawgeti(L, LUA_REGISTRYINDEX, m_provider->tableRef); + lua_getfield(L, -1, "layout_msg"); + lua_remove(L, -2); + + if (!lua_isfunction(L, -1)) { + lua_settop(L, top); + return {}; + } + + pushLayoutContext(L, targets, space->workArea()); + lua_pushlstring(L, sv.data(), sv.size()); + + const int status = m_provider->manager->guardedPCall(2, 1, 0, CConfigManager::LUA_TIMEOUT_LAYOUT_CALLBACK_MS, "lua layout_msg callback"); + if (status != LUA_OK) { + std::string err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "unknown lua error"; + lua_settop(L, top); + reportError(err); + return Config::configError(std::format("lua layout {} layout_msg failed: {}", m_provider->name, err), Config::eConfigErrorLevel::ERROR, + Config::eConfigErrorCode::LUA_ERROR); + } + + Config::ErrorResult result = {}; + if (lua_isboolean(L, -1) && !lua_toboolean(L, -1)) + result = + Config::configError(std::format("lua layout {} rejected layoutmsg", m_provider->name), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); + else if (lua_isstring(L, -1)) + result = Config::configError(lua_tostring(L, -1), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); + + lua_settop(L, top); + recalculate(); + return result; +} + +std::optional CLuaTiledAlgorithm::predictSizeForNewTarget() { + auto parent = m_parent.lock(); + auto space = parent ? parent->space() : nullptr; + if (!space) + return std::nullopt; + return space->workArea().size(); +} + +SP CLuaTiledAlgorithm::getNextCandidate(SP old) { + auto targets = liveTargets(); + if (targets.empty()) + return nullptr; + + auto it = std::ranges::find(targets, old); + if (it == targets.end()) + return targets.back(); + + if (targets.size() == 1) + return nullptr; + + auto next = std::next(it); + if (next == targets.end()) + next = targets.begin(); + + return *next; +} + +std::optional CLuaTiledAlgorithm::layoutName() const { + if (!m_provider) + return std::nullopt; + return m_provider->name; +} + +std::vector> CLuaTiledAlgorithm::liveTargets() { + std::erase_if(m_targets, [](const auto& target) { return !target || !target.lock(); }); + + std::vector> result; + for (const auto& target : m_targets) { + if (const auto locked = target.lock(); locked) + result.emplace_back(locked); + } + return result; +} + +bool CLuaTiledAlgorithm::callRecalculate(const std::vector>& targets) { + if (!m_provider || !m_provider->active || !m_provider->state || !m_provider->manager) + return false; + + auto parent = m_parent.lock(); + auto space = parent ? parent->space() : nullptr; + if (!space) + return false; + + lua_State* L = m_provider->state; + const int top = lua_gettop(L); + + lua_rawgeti(L, LUA_REGISTRYINDEX, m_provider->tableRef); + lua_getfield(L, -1, "recalculate"); + lua_remove(L, -2); + + if (!lua_isfunction(L, -1)) { + lua_settop(L, top); + return false; + } + + pushLayoutContext(L, targets, space->workArea()); + + const int status = m_provider->manager->guardedPCall(1, 0, 0, CConfigManager::LUA_TIMEOUT_LAYOUT_CALLBACK_MS, "lua layout recalculate callback"); + if (status != LUA_OK) { + std::string err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "unknown lua error"; + lua_settop(L, top); + reportError(err); + return false; + } + + lua_settop(L, top); + return true; +} + +void CLuaTiledAlgorithm::applyDefaultGrid(const std::vector>& targets) { + auto parent = m_parent.lock(); + auto space = parent ? parent->space() : nullptr; + if (!space || targets.empty()) + return; + + const auto AREA = space->workArea(); + const int cols = std::max(1, sc(std::ceil(std::sqrt(sc(targets.size()))))); + const int rows = std::max(1, sc(std::ceil(sc(targets.size()) / sc(cols)))); + + for (size_t i = 0; i < targets.size(); ++i) { + const int col = sc(i) % cols; + const int row = sc(i) / cols; + targets[i]->setPositionGlobal(CBox{AREA.x + AREA.w * col / cols, AREA.y + AREA.h * row / rows, AREA.w / cols, AREA.h / rows}.noNegativeSize()); + } +} + +void CLuaTiledAlgorithm::reportError(const std::string& message) { + if (!m_provider) + return; + + Log::logger->log(Log::ERR, "[lua] layout {} error: {}", m_provider->name, message); + + if (m_provider->didError || !m_provider->manager) + return; + + m_provider->didError = true; + m_provider->manager->addError(std::format("lua layout {} error: {}", m_provider->name, message)); +} + +std::expected CConfigManager::registerLuaLayoutProvider(std::string name, lua_State* L, int providerTableIdx) { + if (name.empty()) + return std::unexpected("layout name cannot be empty"); + + name = normalizeLuaLayoutName(std::move(name)); + if (name == "lua:") + return std::unexpected("layout name cannot be empty"); + + providerTableIdx = lua_absindex(L, providerTableIdx); + if (!lua_istable(L, providerTableIdx)) + return std::unexpected("provider must be a table"); + + lua_getfield(L, providerTableIdx, "recalculate"); + const bool hasRecalculate = lua_isfunction(L, -1); + lua_pop(L, 1); + + if (!hasRecalculate) + return std::unexpected("provider table must define recalculate(ctx)"); + + lua_pushvalue(L, providerTableIdx); + const int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + auto provider = makeShared(); + provider->manager = this; + provider->state = L; + provider->name = name; + provider->tableRef = ref; + + if (!Layout::Supplementary::algoMatcher()->registerTiledAlgo(name, &typeid(CLuaTiledAlgorithm), [provider] { return makeUnique(provider); })) { + luaL_unref(L, LUA_REGISTRYINDEX, ref); + return std::unexpected(std::format("layout '{}' is already registered", name)); + } + + m_luaLayoutProviders.emplace_back(provider); + return {}; +} + +void CConfigManager::clearLuaLayoutProviders() { + if (m_luaLayoutProviders.empty()) + return; + + for (auto& provider : m_luaLayoutProviders) { + if (provider) + provider->active = false; + } + + for (auto& provider : m_luaLayoutProviders) { + if (provider) + Layout::Supplementary::algoMatcher()->unregisterAlgo(provider->name); + } + + for (auto& provider : m_luaLayoutProviders) { + if (provider && m_lua && provider->tableRef != LUA_NOREF) { + luaL_unref(m_lua, LUA_REGISTRYINDEX, provider->tableRef); + provider->tableRef = LUA_NOREF; + } + } + + m_luaLayoutProviders.clear(); +} + +static int hlLayoutRegister(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + const char* name = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TTABLE); + + auto result = mgr->registerLuaLayoutProvider(name, L, 2); + if (!result) + return Config::Lua::Bindings::Internal::configError(L, "hl.layout.register: {}", result.error()); + + return 0; +} + +void Config::Lua::Bindings::Internal::registerLayoutBindings(lua_State* L, CConfigManager* mgr) { + setupLayoutTarget(L); + + lua_newtable(L); + setMgrFn(L, mgr, "register", hlLayoutRegister); + lua_setfield(L, -2, "layout"); +} diff --git a/src/config/lua/layout/LuaLayoutProvider.hpp b/src/config/lua/layout/LuaLayoutProvider.hpp new file mode 100644 index 000000000..4e223670c --- /dev/null +++ b/src/config/lua/layout/LuaLayoutProvider.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "../../../helpers/memory/Memory.hpp" +#include "../../../helpers/math/Math.hpp" +#include "../../../layout/algorithm/TiledAlgorithm.hpp" + +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace Config::Lua { + class CConfigManager; +} + +namespace Config::Lua::Layouts { + + struct SLuaLayoutProvider { + CConfigManager* manager = nullptr; + lua_State* state = nullptr; + std::string name; + int tableRef = LUA_NOREF; + bool active = true; + bool didError = false; + }; + + class CLuaTiledAlgorithm : public Layout::ITiledAlgorithm { + public: + explicit CLuaTiledAlgorithm(SP provider); + virtual ~CLuaTiledAlgorithm() = default; + + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); + virtual void resizeTarget(const Vector2D& Δ, SP target, Layout::eRectCorner corner = Layout::CORNER_NONE); + virtual void recalculate(); + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); + virtual SP getNextCandidate(SP old); + virtual std::optional layoutName() const; + + private: + SP m_provider; + std::vector> m_targets; + + std::vector> liveTargets(); + bool callRecalculate(const std::vector>& targets); + void applyDefaultGrid(const std::vector>& targets); + void reportError(const std::string& message); + }; + +} diff --git a/src/config/lua/layout/LuaLayoutTarget.cpp b/src/config/lua/layout/LuaLayoutTarget.cpp new file mode 100644 index 000000000..9113f0fd5 --- /dev/null +++ b/src/config/lua/layout/LuaLayoutTarget.cpp @@ -0,0 +1,98 @@ +#include "LuaLayoutTarget.hpp" + +#include "LuaLayoutContext.hpp" +#include "../bindings/LuaBindingsInternal.hpp" +#include "../objects/LuaObjectHelpers.hpp" +#include "../objects/LuaWindow.hpp" + +#include "../../../layout/target/Target.hpp" + +#include + +using namespace Config::Lua; +using namespace Config::Lua::Layouts; + +static constexpr const char* TARGET_MT = "HL.LayoutTarget"; + +namespace { + struct SLuaLayoutTargetRef { + WP target; + size_t index = 0; + }; +} + +static int layoutTargetPlace(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, TARGET_MT)); + auto target = ref->target.lock(); + if (!target) + return 0; + + CBox box; + if (!boxFromTable(L, 2, box)) + return Bindings::Internal::configError(L, "HL.LayoutTarget: place expects a box table { x, y, w, h }"); + + target->setPositionGlobal(box.noNegativeSize()); + return 0; +} + +static int layoutTargetIndex(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, TARGET_MT)); + const std::string_view key = luaL_checkstring(L, 2); + + auto target = ref->target.lock(); + if (!target) { + lua_pushnil(L); + return 1; + } + + if (key == "index") + lua_pushinteger(L, sc(ref->index)); + else if (key == "window") { + const auto window = target->window(); + if (window) + Objects::CLuaWindow::push(L, window); + else + lua_pushnil(L); + } else if (key == "box") + pushBox(L, target->position()); + else if (key == "place" || key == "set_box") + lua_pushcfunction(L, layoutTargetPlace); + else + lua_pushnil(L); + + return 1; +} + +static int layoutTargetGc(lua_State* L) { + sc(lua_touserdata(L, 1))->~SLuaLayoutTargetRef(); + return 0; +} + +static int layoutTargetToString(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, TARGET_MT)); + auto target = ref->target.lock(); + if (!target) + lua_pushstring(L, "HL.LayoutTarget(expired)"); + else + lua_pushfstring(L, "HL.LayoutTarget(%d)", sc(ref->index)); + return 1; +} + +void Config::Lua::Layouts::setupLayoutTarget(lua_State* L) { + luaL_newmetatable(L, TARGET_MT); + lua_pushcfunction(L, layoutTargetIndex); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, Objects::readOnlyNewIndex); + lua_setfield(L, -2, "__newindex"); + lua_pushcfunction(L, layoutTargetGc); + lua_setfield(L, -2, "__gc"); + lua_pushcfunction(L, layoutTargetToString); + lua_setfield(L, -2, "__tostring"); + lua_pop(L, 1); +} + +void Config::Lua::Layouts::pushLayoutTarget(lua_State* L, const SP& target, size_t index) { + new (lua_newuserdata(L, sizeof(SLuaLayoutTargetRef))) SLuaLayoutTargetRef{target, index}; + luaL_getmetatable(L, TARGET_MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/layout/LuaLayoutTarget.hpp b/src/config/lua/layout/LuaLayoutTarget.hpp new file mode 100644 index 000000000..5f23e74a5 --- /dev/null +++ b/src/config/lua/layout/LuaLayoutTarget.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "../../../helpers/memory/Memory.hpp" + +#include + +extern "C" { +#include +} + +namespace Layout { + class ITarget; +} + +namespace Config::Lua::Layouts { + void setupLayoutTarget(lua_State* L); + void pushLayoutTarget(lua_State* L, const SP& target, size_t index); +} diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp index 7fe87334d..41edcc787 100644 --- a/src/config/lua/objects/LuaWindow.cpp +++ b/src/config/lua/objects/LuaWindow.cpp @@ -176,7 +176,7 @@ static int windowIndex(lua_State* L) { } const auto& tiledAlgo = algo->tiledAlgo(); - const std::string name = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*tiledAlgo.get())); + const std::string name = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(tiledAlgo.get()); lua_newtable(L); lua_pushstring(L, name.c_str()); diff --git a/src/config/lua/objects/LuaWorkspace.cpp b/src/config/lua/objects/LuaWorkspace.cpp index 3f89b41a1..8cfb32e71 100644 --- a/src/config/lua/objects/LuaWorkspace.cpp +++ b/src/config/lua/objects/LuaWorkspace.cpp @@ -136,7 +136,7 @@ static int workspaceIndex(lua_State* L) { 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())); + layoutName = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(TILED_ALGO.get()); } lua_pushstring(L, layoutName.c_str()); } else if (key == "last_window") { diff --git a/src/layout/algorithm/TiledAlgorithm.hpp b/src/layout/algorithm/TiledAlgorithm.hpp index 99d1bd993..6905ac747 100644 --- a/src/layout/algorithm/TiledAlgorithm.hpp +++ b/src/layout/algorithm/TiledAlgorithm.hpp @@ -5,6 +5,9 @@ #include "ModeAlgorithm.hpp" +#include +#include + namespace Layout { class ITarget; @@ -16,6 +19,12 @@ namespace Layout { virtual SP getNextCandidate(SP old) = 0; + // Optional runtime layout name. Useful for generic adapter classes where + // typeid alone cannot identify the selected layout instance. + virtual std::optional layoutName() const { + return std::nullopt; + } + protected: ITiledAlgorithm() = default; @@ -23,4 +32,4 @@ namespace Layout { friend class Layout::CAlgorithm; }; -} \ No newline at end of file +} diff --git a/src/layout/supplementary/WorkspaceAlgoMatcher.cpp b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp index fe8680380..08744e129 100644 --- a/src/layout/supplementary/WorkspaceAlgoMatcher.cpp +++ b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp @@ -4,6 +4,7 @@ #include "../../config/shared/workspace/WorkspaceRuleManager.hpp" #include "../algorithm/Algorithm.hpp" +#include "../algorithm/TiledAlgorithm.hpp" #include "../space/Space.hpp" #include "../algorithm/floating/default/DefaultFloatingAlgorithm.hpp" @@ -124,7 +125,9 @@ void CWorkspaceAlgoMatcher::updateWorkspaceLayouts() { const auto LAYOUT_TO_USE = tiledAlgoForWorkspace(ws.lock()); - if (m_algoNames.contains(&typeid(*TILED_ALGO.get())) && m_algoNames.at(&typeid(*TILED_ALGO.get())) == LAYOUT_TO_USE) + const auto CURRENT_LAYOUT = getNameForTiledAlgo(TILED_ALGO.get()); + + if (CURRENT_LAYOUT == LAYOUT_TO_USE && m_tiledAlgos.contains(LAYOUT_TO_USE)) continue; // needs a switchup @@ -132,6 +135,16 @@ void CWorkspaceAlgoMatcher::updateWorkspaceLayouts() { } } +std::string CWorkspaceAlgoMatcher::getNameForTiledAlgo(const ITiledAlgorithm* algo) { + if (!algo) + return "unknown"; + + if (const auto name = algo->layoutName(); name.has_value()) + return *name; + + return getNameForTiledAlgo(&typeid(*algo)); +} + std::string CWorkspaceAlgoMatcher::getNameForTiledAlgo(const std::type_info* type) { if (m_algoNames.contains(type)) return m_algoNames.at(type); diff --git a/src/layout/supplementary/WorkspaceAlgoMatcher.hpp b/src/layout/supplementary/WorkspaceAlgoMatcher.hpp index d39e29988..bdc45be53 100644 --- a/src/layout/supplementary/WorkspaceAlgoMatcher.hpp +++ b/src/layout/supplementary/WorkspaceAlgoMatcher.hpp @@ -21,6 +21,7 @@ namespace Layout::Supplementary { SP createAlgorithmForWorkspace(PHLWORKSPACE w); void updateWorkspaceLayouts(); + std::string getNameForTiledAlgo(const Layout::ITiledAlgorithm* algo); std::string getNameForTiledAlgo(const std::type_info* type); // these fns can fail due to name collisions @@ -43,4 +44,4 @@ namespace Layout::Supplementary { }; const UP& algoMatcher(); -} \ No newline at end of file +}