summaryrefslogtreecommitdiff
path: root/src/config/lua/layout
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-05-06 13:56:12 +0100
committerGitHub <noreply@github.com>2026-05-06 13:56:12 +0100
commit1681bea42dd2f11ba3fe6df05560d0b231de3c76 (patch)
tree899103dbddf675a01758c7cdd379224c9c81a7fb /src/config/lua/layout
parent5f1350f522d3fe3af78cbe30a98311763b3509d5 (diff)
config/lua: add simple layout API (#14258)
Adds a simple layout lua api
Diffstat (limited to 'src/config/lua/layout')
-rw-r--r--src/config/lua/layout/LuaLayoutContext.cpp144
-rw-r--r--src/config/lua/layout/LuaLayoutContext.hpp20
-rw-r--r--src/config/lua/layout/LuaLayoutProvider.cpp323
-rw-r--r--src/config/lua/layout/LuaLayoutProvider.hpp59
-rw-r--r--src/config/lua/layout/LuaLayoutTarget.cpp98
-rw-r--r--src/config/lua/layout/LuaLayoutTarget.hpp18
6 files changed, 662 insertions, 0 deletions
diff --git a/src/config/lua/layout/LuaLayoutContext.cpp b/src/config/lua/layout/LuaLayoutContext.cpp
new file mode 100644
index 00000000..f7370e1b
--- /dev/null
+++ b/src/config/lua/layout/LuaLayoutContext.cpp
@@ -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");
+}
diff --git a/src/config/lua/layout/LuaLayoutContext.hpp b/src/config/lua/layout/LuaLayoutContext.hpp
new file mode 100644
index 00000000..2f997037
--- /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 <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);
+}
diff --git a/src/config/lua/layout/LuaLayoutProvider.cpp b/src/config/lua/layout/LuaLayoutProvider.cpp
new file mode 100644
index 00000000..344362ff
--- /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 <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");
+}
diff --git a/src/config/lua/layout/LuaLayoutProvider.hpp b/src/config/lua/layout/LuaLayoutProvider.hpp
new file mode 100644
index 00000000..4e223670
--- /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 <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);
+ };
+
+}
diff --git a/src/config/lua/layout/LuaLayoutTarget.cpp b/src/config/lua/layout/LuaLayoutTarget.cpp
new file mode 100644
index 00000000..9113f0fd
--- /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 <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);
+}
diff --git a/src/config/lua/layout/LuaLayoutTarget.hpp b/src/config/lua/layout/LuaLayoutTarget.hpp
new file mode 100644
index 00000000..5f23e74a
--- /dev/null
+++ b/src/config/lua/layout/LuaLayoutTarget.hpp
@@ -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);
+}