mirror of
https://github.com/hyprwm/Hyprland
synced 2026-05-07 19:38:01 +02:00
config/lua: add simple layout API (#14258)
Adds a simple layout lua api
This commit is contained in:
parent
5f1350f522
commit
1681bea42d
23 changed files with 1127 additions and 31 deletions
12
example/layouts/columns.lua
Normal file
12
example/layouts/columns.lua
Normal file
|
|
@ -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,
|
||||
})
|
||||
14
example/layouts/grid.lua
Normal file
14
example/layouts/grid.lua
Normal file
|
|
@ -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,
|
||||
})
|
||||
144
example/layouts/manual.lua
Normal file
144
example/layouts/manual.lua
Normal file
|
|
@ -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,
|
||||
})
|
||||
55
example/layouts/spiral.lua
Normal file
55
example/layouts/spiral.lua
Normal file
|
|
@ -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,
|
||||
})
|
||||
56
hyprtester/src/tests/main/layout_custom.cpp
Normal file
56
hyprtester/src/tests/main/layout_custom.cpp
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string|number|boolean>): 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",
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <chrono>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <expected>
|
||||
|
||||
#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<void, std::string> registerLuaLayoutProvider(std::string name, lua_State* L, int providerTableIdx);
|
||||
|
||||
// execute an arbitrary lua string on the current state.
|
||||
std::optional<std::string> 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<std::string, SP<Desktop::Rule::CLayerRule>> 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<void, std::string> registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name);
|
||||
std::expected<void, std::string> 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<void, std::string> registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name);
|
||||
std::expected<void, std::string> 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<int> m_heldLuaRefs;
|
||||
std::vector<int> m_heldLuaRefs;
|
||||
std::vector<SP<Layouts::SLuaLayoutProvider>> m_luaLayoutProviders;
|
||||
|
||||
// this is here for legacy reasons.
|
||||
std::unordered_map<std::string, const void*> m_configPtrMap;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
144
src/config/lua/layout/LuaLayoutContext.cpp
Normal file
144
src/config/lua/layout/LuaLayoutContext.cpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#include "LuaLayoutContext.hpp"
|
||||
|
||||
#include "LuaLayoutTarget.hpp"
|
||||
#include "../bindings/LuaBindingsInternal.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string_view>
|
||||
|
||||
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<int>(luaL_checkinteger(L, 2)));
|
||||
const int cols = std::max(1, sc<int>(luaL_checkinteger(L, 3)));
|
||||
int rows = 0;
|
||||
|
||||
if (lua_gettop(L) >= 4 && lua_isnumber(L, 4))
|
||||
rows = std::max(1, sc<int>(lua_tointeger(L, 4)));
|
||||
else {
|
||||
const auto count = std::max<size_t>(1, targetCountFromContext(L, 1));
|
||||
rows = std::max(1, sc<int>(std::ceil(sc<double>(count) / sc<double>(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<int>(luaL_checkinteger(L, 2)));
|
||||
const int n = std::max(1, sc<int>(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<int>(luaL_checkinteger(L, 2)));
|
||||
const int n = std::max(1, sc<int>(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<SP<Layout::ITarget>>& 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");
|
||||
}
|
||||
20
src/config/lua/layout/LuaLayoutContext.hpp
Normal file
20
src/config/lua/layout/LuaLayoutContext.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../../helpers/math/Math.hpp"
|
||||
#include "../../../helpers/memory/Memory.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
}
|
||||
|
||||
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<SP<Layout::ITarget>>& targets, const CBox& area);
|
||||
}
|
||||
323
src/config/lua/layout/LuaLayoutProvider.cpp
Normal file
323
src/config/lua/layout/LuaLayoutProvider.cpp
Normal file
|
|
@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
|
||||
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<SLuaLayoutProvider> provider) : m_provider(std::move(provider)) {
|
||||
;
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::newTarget(SP<Layout::ITarget> target) {
|
||||
m_targets.emplace_back(target);
|
||||
recalculate();
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::movedTarget(SP<Layout::ITarget> target, std::optional<Vector2D> focalPoint) {
|
||||
newTarget(target);
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::removeTarget(SP<Layout::ITarget> target) {
|
||||
std::erase_if(m_targets, [&target](const auto& t) { return !t || t.lock() == target; });
|
||||
recalculate();
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::resizeTarget(const Vector2D& Δ, SP<Layout::ITarget> target, Layout::eRectCorner corner) {
|
||||
recalculate();
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::recalculate() {
|
||||
auto targets = liveTargets();
|
||||
if (targets.empty())
|
||||
return;
|
||||
|
||||
if (!callRecalculate(targets))
|
||||
applyDefaultGrid(targets);
|
||||
}
|
||||
|
||||
void CLuaTiledAlgorithm::swapTargets(SP<Layout::ITarget> a, SP<Layout::ITarget> 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<Layout::ITarget> 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<Vector2D> CLuaTiledAlgorithm::predictSizeForNewTarget() {
|
||||
auto parent = m_parent.lock();
|
||||
auto space = parent ? parent->space() : nullptr;
|
||||
if (!space)
|
||||
return std::nullopt;
|
||||
return space->workArea().size();
|
||||
}
|
||||
|
||||
SP<Layout::ITarget> CLuaTiledAlgorithm::getNextCandidate(SP<Layout::ITarget> 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<std::string> CLuaTiledAlgorithm::layoutName() const {
|
||||
if (!m_provider)
|
||||
return std::nullopt;
|
||||
return m_provider->name;
|
||||
}
|
||||
|
||||
std::vector<SP<Layout::ITarget>> CLuaTiledAlgorithm::liveTargets() {
|
||||
std::erase_if(m_targets, [](const auto& target) { return !target || !target.lock(); });
|
||||
|
||||
std::vector<SP<Layout::ITarget>> 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<SP<Layout::ITarget>>& 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<SP<Layout::ITarget>>& 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<int>(std::ceil(std::sqrt(sc<double>(targets.size())))));
|
||||
const int rows = std::max(1, sc<int>(std::ceil(sc<double>(targets.size()) / sc<double>(cols))));
|
||||
|
||||
for (size_t i = 0; i < targets.size(); ++i) {
|
||||
const int col = sc<int>(i) % cols;
|
||||
const int row = sc<int>(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<void, std::string> 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<SLuaLayoutProvider>();
|
||||
provider->manager = this;
|
||||
provider->state = L;
|
||||
provider->name = name;
|
||||
provider->tableRef = ref;
|
||||
|
||||
if (!Layout::Supplementary::algoMatcher()->registerTiledAlgo(name, &typeid(CLuaTiledAlgorithm), [provider] { return makeUnique<CLuaTiledAlgorithm>(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<CConfigManager*>(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");
|
||||
}
|
||||
59
src/config/lua/layout/LuaLayoutProvider.hpp
Normal file
59
src/config/lua/layout/LuaLayoutProvider.hpp
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../../helpers/memory/Memory.hpp"
|
||||
#include "../../../helpers/math/Math.hpp"
|
||||
#include "../../../layout/algorithm/TiledAlgorithm.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
}
|
||||
|
||||
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<SLuaLayoutProvider> provider);
|
||||
virtual ~CLuaTiledAlgorithm() = default;
|
||||
|
||||
virtual void newTarget(SP<Layout::ITarget> target);
|
||||
virtual void movedTarget(SP<Layout::ITarget> target, std::optional<Vector2D> focalPoint = std::nullopt);
|
||||
virtual void removeTarget(SP<Layout::ITarget> target);
|
||||
virtual void resizeTarget(const Vector2D& Δ, SP<Layout::ITarget> target, Layout::eRectCorner corner = Layout::CORNER_NONE);
|
||||
virtual void recalculate();
|
||||
virtual void swapTargets(SP<Layout::ITarget> a, SP<Layout::ITarget> b);
|
||||
virtual void moveTargetInDirection(SP<Layout::ITarget> t, Math::eDirection dir, bool silent);
|
||||
virtual Config::ErrorResult layoutMsg(const std::string_view& sv);
|
||||
virtual std::optional<Vector2D> predictSizeForNewTarget();
|
||||
virtual SP<Layout::ITarget> getNextCandidate(SP<Layout::ITarget> old);
|
||||
virtual std::optional<std::string> layoutName() const;
|
||||
|
||||
private:
|
||||
SP<SLuaLayoutProvider> m_provider;
|
||||
std::vector<WP<Layout::ITarget>> m_targets;
|
||||
|
||||
std::vector<SP<Layout::ITarget>> liveTargets();
|
||||
bool callRecalculate(const std::vector<SP<Layout::ITarget>>& targets);
|
||||
void applyDefaultGrid(const std::vector<SP<Layout::ITarget>>& targets);
|
||||
void reportError(const std::string& message);
|
||||
};
|
||||
|
||||
}
|
||||
98
src/config/lua/layout/LuaLayoutTarget.cpp
Normal file
98
src/config/lua/layout/LuaLayoutTarget.cpp
Normal file
|
|
@ -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 <string_view>
|
||||
|
||||
using namespace Config::Lua;
|
||||
using namespace Config::Lua::Layouts;
|
||||
|
||||
static constexpr const char* TARGET_MT = "HL.LayoutTarget";
|
||||
|
||||
namespace {
|
||||
struct SLuaLayoutTargetRef {
|
||||
WP<Layout::ITarget> target;
|
||||
size_t index = 0;
|
||||
};
|
||||
}
|
||||
|
||||
static int layoutTargetPlace(lua_State* L) {
|
||||
auto* ref = sc<SLuaLayoutTargetRef*>(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<SLuaLayoutTargetRef*>(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<lua_Integer>(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<SLuaLayoutTargetRef*>(lua_touserdata(L, 1))->~SLuaLayoutTargetRef();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layoutTargetToString(lua_State* L) {
|
||||
auto* ref = sc<SLuaLayoutTargetRef*>(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<int>(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<Layout::ITarget>& target, size_t index) {
|
||||
new (lua_newuserdata(L, sizeof(SLuaLayoutTargetRef))) SLuaLayoutTargetRef{target, index};
|
||||
luaL_getmetatable(L, TARGET_MT);
|
||||
lua_setmetatable(L, -2);
|
||||
}
|
||||
18
src/config/lua/layout/LuaLayoutTarget.hpp
Normal file
18
src/config/lua/layout/LuaLayoutTarget.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../../helpers/memory/Memory.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
}
|
||||
|
||||
namespace Layout {
|
||||
class ITarget;
|
||||
}
|
||||
|
||||
namespace Config::Lua::Layouts {
|
||||
void setupLayoutTarget(lua_State* L);
|
||||
void pushLayoutTarget(lua_State* L, const SP<Layout::ITarget>& target, size_t index);
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
|
||||
#include "ModeAlgorithm.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace Layout {
|
||||
|
||||
class ITarget;
|
||||
|
|
@ -16,6 +19,12 @@ namespace Layout {
|
|||
|
||||
virtual SP<ITarget> getNextCandidate(SP<ITarget> old) = 0;
|
||||
|
||||
// Optional runtime layout name. Useful for generic adapter classes where
|
||||
// typeid alone cannot identify the selected layout instance.
|
||||
virtual std::optional<std::string> layoutName() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
protected:
|
||||
ITiledAlgorithm() = default;
|
||||
|
||||
|
|
@ -23,4 +32,4 @@ namespace Layout {
|
|||
|
||||
friend class Layout::CAlgorithm;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace Layout::Supplementary {
|
|||
|
||||
SP<CAlgorithm> 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<CWorkspaceAlgoMatcher>& algoMatcher();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue