Files
hyprland-columns-plugin/src/main.cpp
T

222 lines
7.2 KiB
C++

#include <algorithm>
#include <expected>
#include <string>
#include <string_view>
#include <vector>
#include <hyprland/src/helpers/Color.hpp>
#include <hyprland/src/layout/algorithm/TiledAlgorithm.hpp>
#include <hyprland/src/layout/algorithm/Algorithm.hpp>
#include <hyprland/src/layout/space/Space.hpp>
#include <hyprland/src/layout/target/Target.hpp>
#include <hyprland/src/plugins/PluginAPI.hpp>
static HANDLE g_pluginHandle = nullptr;
namespace Layout::Tiled {
class CColumnsAlgorithm final : public ITiledAlgorithm {
public:
void newTarget(SP<ITarget> target) override {
addTarget(std::move(target));
recalculate();
}
void movedTarget(SP<ITarget> target, std::optional<Vector2D> /*focalPoint*/ = std::nullopt) override {
addTarget(std::move(target));
recalculate();
}
void removeTarget(SP<ITarget> target) override {
cleanup();
std::erase_if(m_targets, [&](const WP<ITarget>& weak) {
const auto locked = weak.lock();
return !locked || locked == target;
});
recalculate();
}
void resizeTarget(const Vector2D& /*delta*/, SP<ITarget> /*target*/, eRectCorner /*corner*/ = CORNER_NONE) override {
// Intentionally ignored: this layout always keeps equal-width columns.
}
void recalculate(eRecalculateReason /*reason*/ = RECALCULATE_REASON_UNKNOWN) override {
cleanup();
const auto parent = m_parent.lock();
if (!parent)
return;
const auto space = parent->space();
if (!space)
return;
std::vector<SP<ITarget>> tiledTargets;
tiledTargets.reserve(m_targets.size());
for (auto const& weak : m_targets) {
const auto target = weak.lock();
if (target)
tiledTargets.push_back(target);
}
if (tiledTargets.empty())
return;
const auto area = space->workArea(false);
const auto count = tiledTargets.size();
for (size_t i = 0; i < count; ++i) {
const double left = area.x + (area.w * static_cast<double>(i) / static_cast<double>(count));
const double right = area.x + (area.w * static_cast<double>(i + 1) / static_cast<double>(count));
const CBox box(left, area.y, right - left, area.h);
tiledTargets[i]->setPositionGlobal(box);
tiledTargets[i]->recalc();
}
}
SP<ITarget> getNextCandidate(SP<ITarget> old) override {
cleanup();
auto targets = liveTargets();
if (targets.empty())
return nullptr;
if (!old)
return targets.front();
const auto it = std::ranges::find(targets, old);
if (it == targets.end())
return targets.front();
const auto idx = static_cast<size_t>(std::distance(targets.begin(), it));
return targets[(idx + 1) % targets.size()];
}
Config::ErrorResult layoutMsg(const std::string_view& sv) override {
if (sv == "togglesplit" || sv == "swapsplit")
return {};
if (sv == "recalculate") {
recalculate();
return {};
}
return Config::configError(std::string{"columns: unsupported layout message: "} + std::string{sv},
Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT);
}
std::optional<Vector2D> predictSizeForNewTarget() override {
const auto parent = m_parent.lock();
if (!parent)
return std::nullopt;
const auto space = parent->space();
if (!space)
return std::nullopt;
const auto area = space->workArea(false);
const auto n = std::max<size_t>(1, liveTargets().size() + 1);
return Vector2D(area.w / static_cast<double>(n), area.h);
}
void swapTargets(SP<ITarget> a, SP<ITarget> b) override {
cleanup();
auto ia = indexOf(a);
auto ib = indexOf(b);
if (!ia.has_value() || !ib.has_value() || ia == ib)
return;
std::swap(m_targets[*ia], m_targets[*ib]);
recalculate();
}
void moveTargetInDirection(SP<ITarget> t, Math::eDirection dir, bool /*silent*/) override {
cleanup();
const auto idx = indexOf(t);
if (!idx.has_value())
return;
if (dir == Math::DIRECTION_LEFT && *idx > 0) {
std::swap(m_targets[*idx], m_targets[*idx - 1]);
recalculate();
return;
}
if (dir == Math::DIRECTION_RIGHT && *idx + 1 < m_targets.size()) {
std::swap(m_targets[*idx], m_targets[*idx + 1]);
recalculate();
return;
}
}
private:
std::vector<WP<ITarget>> m_targets;
void addTarget(SP<ITarget> target) {
cleanup();
const auto exists = std::ranges::any_of(m_targets, [&](const WP<ITarget>& weak) {
const auto locked = weak.lock();
return locked && locked == target;
});
if (!exists)
m_targets.emplace_back(target);
}
void cleanup() {
std::erase_if(m_targets, [](const WP<ITarget>& weak) { return weak.expired(); });
}
std::vector<SP<ITarget>> liveTargets() const {
std::vector<SP<ITarget>> out;
out.reserve(m_targets.size());
for (auto const& weak : m_targets) {
const auto locked = weak.lock();
if (locked)
out.push_back(locked);
}
return out;
}
std::optional<size_t> indexOf(SP<ITarget> target) const {
for (size_t i = 0; i < m_targets.size(); ++i) {
const auto locked = m_targets[i].lock();
if (locked && locked == target)
return i;
}
return std::nullopt;
}
};
}
APICALL EXPORT std::string PLUGIN_API_VERSION() {
return HYPRLAND_API_VERSION;
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
g_pluginHandle = handle;
const bool ok = HyprlandAPI::addTiledAlgo(handle, "columns", &typeid(Layout::Tiled::CColumnsAlgorithm), []() -> UP<Layout::ITiledAlgorithm> {
return makeUnique<Layout::Tiled::CColumnsAlgorithm>();
});
if (!ok)
HyprlandAPI::addNotification(handle, "hyprcolumns: failed to register columns layout", CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000);
else
HyprlandAPI::addNotification(handle, "hyprcolumns: registered columns layout", CHyprColor{0.2, 1.0, 0.6, 1.0}, 3000);
return {.name = "hyprcolumns", .description = "Equal-width tiled columns layout", .author = "OpenAI", .version = "0.1.0"};
}
APICALL EXPORT void PLUGIN_EXIT() {
if (g_pluginHandle)
HyprlandAPI::removeAlgo(g_pluginHandle, "columns");
}