diff options
| author | Vaxry <43317083+vaxerski@users.noreply.github.com> | 2026-05-06 13:56:12 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-06 13:56:12 +0100 |
| commit | 1681bea42dd2f11ba3fe6df05560d0b231de3c76 (patch) | |
| tree | 899103dbddf675a01758c7cdd379224c9c81a7fb /src/config/lua/layout/LuaLayoutProvider.cpp | |
| parent | 5f1350f522d3fe3af78cbe30a98311763b3509d5 (diff) | |
config/lua: add simple layout API (#14258)
Adds a simple layout lua api
Diffstat (limited to 'src/config/lua/layout/LuaLayoutProvider.cpp')
| -rw-r--r-- | src/config/lua/layout/LuaLayoutProvider.cpp | 323 |
1 files changed, 323 insertions, 0 deletions
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"); +} |
