This commit is contained in:
Amit Serper 2026-04-28 22:43:49 +05:30 committed by GitHub
commit 461bb232aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 603 additions and 110 deletions

5
.gitignore vendored
View file

@ -39,4 +39,7 @@ result
result-man
build/
.cache/
.cache/
# Claude Code
CLAUDE.md

View file

@ -250,9 +250,24 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:bg_col", Hyprlang::INT{0xFF111111});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method", Hyprlang::STRING{"center current"});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:dynamic_grid", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:gesture_distance", Hyprlang::INT{200});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:active_highlight_col", Hyprlang::INT{0xFF3584E4});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:active_highlight_border", Hyprlang::INT{2});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:hover_highlight_col", Hyprlang::INT{0x80FFFFFF});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:hover_highlight_border", Hyprlang::INT{2});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_pos", Hyprlang::STRING{"top_right"});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_size", Hyprlang::INT{36});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_col", Hyprlang::INT{0xFFFFFFFF});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_names", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:enable_keyboard_nav", Hyprlang::INT{1});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:fill_gaps", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:mru_sort", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:enable_drag_move", Hyprlang::INT{0});
HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:animate_entry", Hyprlang::INT{0});
HyprlandAPI::reloadConfig();
return {"hyprexpo", "A plugin for an overview", "Vaxry", "1.0"};

View file

@ -1,5 +1,10 @@
#include "overview.hpp"
#include <any>
#include <cairo/cairo.h>
#include <pango/pangocairo.h>
#include <xkbcommon/xkbcommon.h>
#include <hyprland/src/devices/IKeyboard.hpp>
#include <hyprland/src/devices/IPointer.hpp>
#define private public
#include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/Compositor.hpp>
@ -10,11 +15,24 @@
#include <hyprland/src/managers/animation/DesktopAnimationManager.hpp>
#include <hyprland/src/managers/cursor/CursorShapeOverrideController.hpp>
#include <hyprland/src/managers/input/InputManager.hpp>
#include <hyprland/src/managers/SeatManager.hpp>
#include <hyprland/src/managers/eventLoop/EventLoopManager.hpp>
#include <hyprland/src/helpers/time/Time.hpp>
#undef private
#include "OverviewPassElement.hpp"
// Compute aspect-correct tile size that fits within a cols x rows grid with gaps
static Vector2D aspectCorrectTileSize(double screenW, double screenH, int cols, int rows, double gapSize) {
double monAspect = screenW / screenH;
double maxTileW = (screenW - gapSize * (cols - 1)) / cols;
double maxTileH = (screenH - gapSize * (rows - 1)) / rows;
double cellAspect = maxTileW / maxTileH;
if (cellAspect > monAspect)
return {maxTileH * monAspect, maxTileH};
else
return {maxTileW, maxTileW / monAspect};
}
static void damageMonitor(WP<Hyprutils::Animation::CBaseAnimatedVariable> thisptr) {
g_pOverview->damage();
}
@ -27,16 +45,40 @@ COverview::~COverview() {
}
COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn_), swipe(swipe_) {
createdAt = std::chrono::steady_clock::now();
const auto PMONITOR = Desktop::focusState()->monitor();
pMonitor = PMONITOR;
static auto* const* PCOLUMNS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:columns")->getDataStaticPtr();
static auto* const* PGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:gap_size")->getDataStaticPtr();
static auto* const* PCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:bg_col")->getDataStaticPtr();
static auto* const* PSKIP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty")->getDataStaticPtr();
static auto const* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr();
static auto* const* PCOLUMNS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:columns")->getDataStaticPtr();
static auto* const* PGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:gap_size")->getDataStaticPtr();
static auto* const* PCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:bg_col")->getDataStaticPtr();
static auto* const* PSKIP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:skip_empty")->getDataStaticPtr();
static auto* const* PDYNAMIC = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:dynamic_grid")->getDataStaticPtr();
static auto const* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr();
static auto* const* PFILLGAPS = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:fill_gaps")->getDataStaticPtr();
static auto* const* PMRUSORT = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:mru_sort")->getDataStaticPtr();
static auto* const* PSHOWNAMES = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:show_workspace_names")->getDataStaticPtr();
SIDE_LENGTH = **PCOLUMNS;
// Dynamic grid: count active workspaces and calculate optimal grid size
if (**PDYNAMIC) {
dynamicGrid = true;
int activeCount = 0;
for (auto& ws : g_pCompositor->m_workspaces) {
if (!ws->m_isSpecialWorkspace && ws->getWindows() > 0)
activeCount++;
}
activeCount = std::max(activeCount, 1);
SIDE_LENGTH = (int)std::ceil(std::sqrt((double)activeCount)); // columns
gridRows = (int)std::ceil((double)activeCount / SIDE_LENGTH); // rows
// Ensure minimum 2x2 grid for better aspect ratio (unless only 1 workspace)
if (activeCount > 1 && gridRows < 2) {
gridRows = 2;
}
} else {
dynamicGrid = false;
SIDE_LENGTH = **PCOLUMNS;
gridRows = SIDE_LENGTH;
}
GAP_WIDTH = **PGAPS;
BG_COLOR = **PCOL;
@ -53,77 +95,115 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
methodStartID = pMonitor->activeWorkspaceID();
}
images.resize(SIDE_LENGTH * SIDE_LENGTH);
// For dynamic grid, collect active workspace IDs; otherwise use standard grid
if (dynamicGrid) {
std::vector<int64_t> activeWorkspaceIDs;
for (auto& ws : g_pCompositor->m_workspaces) {
if (!ws->m_isSpecialWorkspace && ws->getWindows() > 0)
activeWorkspaceIDs.push_back(ws->m_id);
}
std::sort(activeWorkspaceIDs.begin(), activeWorkspaceIDs.end());
if (activeWorkspaceIDs.empty())
activeWorkspaceIDs.push_back(pMonitor->activeWorkspaceID());
// r includes empty workspaces; m skips over them
std::string selector = **PSKIP ? "m" : "r";
if (methodCenter) {
int currentID = methodStartID;
int firstID = currentID;
int backtracked = 0;
// Initialize tiles to WORKSPACE_INVALID; cliking one of these results
// in changing to "emptynm" (next empty workspace). Tiles with this id
// will only remain if skip_empty is on.
for (size_t i = 0; i < images.size(); i++) {
images[i].workspaceID = WORKSPACE_INVALID;
// Fill gaps: include all workspace IDs between min and max
if (**PFILLGAPS && !activeWorkspaceIDs.empty()) {
int64_t minID = activeWorkspaceIDs.front();
int64_t maxID = activeWorkspaceIDs.back();
activeWorkspaceIDs.clear();
for (int64_t id = minID; id <= maxID; ++id)
activeWorkspaceIDs.push_back(id);
}
// Scan through workspaces lower than methodStartID until we wrap; count how many
for (size_t i = 1; i < images.size() / 2; ++i) {
currentID = getWorkspaceIDNameFromString(selector + "-" + std::to_string(i)).id;
if (currentID >= firstID)
break;
backtracked++;
firstID = currentID;
}
// Scan through workspaces higher than methodStartID. If using "m"
// (skip_empty), stop when we wrap, leaving the rest of the workspace
// ID's set to WORKSPACE_INVALID
for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
auto& image = images[i];
if ((int64_t)i - backtracked < 0) {
currentID = getWorkspaceIDNameFromString(selector + std::to_string((int64_t)i - backtracked)).id;
} else {
currentID = getWorkspaceIDNameFromString(selector + "+" + std::to_string((int64_t)i - backtracked)).id;
if (i > 0 && currentID == firstID)
break;
// MRU sort: move current workspace to front
if (**PMRUSORT) {
int64_t currentWsID = startedOn ? startedOn->m_id : pMonitor->activeWorkspaceID();
auto it = std::find(activeWorkspaceIDs.begin(), activeWorkspaceIDs.end(), currentWsID);
if (it != activeWorkspaceIDs.end() && it != activeWorkspaceIDs.begin()) {
int64_t val = *it;
activeWorkspaceIDs.erase(it);
activeWorkspaceIDs.insert(activeWorkspaceIDs.begin(), val);
}
image.workspaceID = currentID;
}
// Recalculate grid dimensions after fill_gaps may have changed the count
int activeCount = activeWorkspaceIDs.size();
SIDE_LENGTH = (int)std::ceil(std::sqrt((double)activeCount));
gridRows = (int)std::ceil((double)activeCount / SIDE_LENGTH);
if (activeCount > 1 && gridRows < 2)
gridRows = 2;
images.resize(activeWorkspaceIDs.size());
for (size_t i = 0; i < activeWorkspaceIDs.size(); ++i)
images[i].workspaceID = activeWorkspaceIDs[i];
} else {
int currentID = methodStartID;
images[0].workspaceID = currentID;
images.resize(SIDE_LENGTH * SIDE_LENGTH);
auto PWORKSPACESTART = g_pCompositor->getWorkspaceByID(currentID);
if (!PWORKSPACESTART)
PWORKSPACESTART = CWorkspace::create(currentID, pMonitor.lock(), std::to_string(currentID));
// r includes empty workspaces; m skips over them
std::string selector = **PSKIP ? "m" : "r";
pMonitor->m_activeWorkspace = PWORKSPACESTART;
if (methodCenter) {
int currentID = methodStartID;
int firstID = currentID;
// Scan through workspaces higher than methodStartID. If using "m"
// (skip_empty), stop when we wrap, leaving the rest of the workspace
// ID's set to WORKSPACE_INVALID
for (size_t i = 1; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
auto& image = images[i];
currentID = getWorkspaceIDNameFromString(selector + "+" + std::to_string(i)).id;
if (currentID <= methodStartID)
break;
image.workspaceID = currentID;
int backtracked = 0;
// Initialize tiles to WORKSPACE_INVALID
for (size_t i = 0; i < images.size(); i++) {
images[i].workspaceID = WORKSPACE_INVALID;
}
// Scan through workspaces lower than methodStartID until we wrap
for (size_t i = 1; i < images.size() / 2; ++i) {
currentID = getWorkspaceIDNameFromString(selector + "-" + std::to_string(i)).id;
if (currentID >= firstID)
break;
backtracked++;
firstID = currentID;
}
// Scan through workspaces higher than methodStartID
for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
auto& image = images[i];
if ((int64_t)i - backtracked < 0) {
currentID = getWorkspaceIDNameFromString(selector + std::to_string((int64_t)i - backtracked)).id;
} else {
currentID = getWorkspaceIDNameFromString(selector + "+" + std::to_string((int64_t)i - backtracked)).id;
if (i > 0 && currentID == firstID)
break;
}
image.workspaceID = currentID;
}
} else {
int currentID = methodStartID;
images[0].workspaceID = currentID;
auto PWORKSPACESTART = g_pCompositor->getWorkspaceByID(currentID);
if (!PWORKSPACESTART)
PWORKSPACESTART = CWorkspace::create(currentID, pMonitor.lock(), std::to_string(currentID));
pMonitor->m_activeWorkspace = PWORKSPACESTART;
for (size_t i = 1; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
auto& image = images[i];
currentID = getWorkspaceIDNameFromString(selector + "+" + std::to_string(i)).id;
if (currentID <= methodStartID)
break;
image.workspaceID = currentID;
}
pMonitor->m_activeWorkspace = startedOn;
}
pMonitor->m_activeWorkspace = startedOn;
}
g_pHyprRenderer->makeEGLCurrent();
Vector2D tileSize = pMonitor->m_size / SIDE_LENGTH;
Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH * pMonitor->m_scale, GAP_WIDTH * pMonitor->m_scale} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {pMonitor->m_size.x / cols, pMonitor->m_size.y / rows};
double scaledGap = GAP_WIDTH * pMonitor->m_scale;
Vector2D tileRenderSize = aspectCorrectTileSize(pMonitor->m_size.x, pMonitor->m_size.y, cols, rows, scaledGap);
CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2};
if (!ENABLE_LOWRES)
@ -139,7 +219,7 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
startedOn->m_visible = false;
for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
for (size_t i = 0; i < images.size(); ++i) {
COverview::SWorkspaceImage& image = images[i];
image.fb.alloc(monbox.w, monbox.h, PMONITOR->m_output->state->state().drmFormat);
@ -172,13 +252,27 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
} else
g_pHyprRenderer->renderWorkspace(PMONITOR, PWORKSPACE, Time::steadyNow(), monbox);
image.box = {(i % SIDE_LENGTH) * tileRenderSize.x + (i % SIDE_LENGTH) * GAP_WIDTH, (i / SIDE_LENGTH) * tileRenderSize.y + (i / SIDE_LENGTH) * GAP_WIDTH, tileRenderSize.x,
tileRenderSize.y};
Vector2D tilePos = tilePosForID(i, pMonitor->m_size, scaledGap);
image.box = {tilePos.x, tilePos.y, tileRenderSize.x, tileRenderSize.y};
g_pHyprOpenGL->m_renderData.blockScreenShader = true;
g_pHyprRenderer->endRender();
}
// Generate workspace labels - ensure EGL context is current for GL calls
g_pHyprRenderer->makeEGLCurrent();
static auto* const* PLABELSIZE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_size")->getDataStaticPtr();
const int LABEL_SIZE = **PLABELSIZE;
for (size_t i = 0; i < images.size(); ++i) {
images[i].labelTex = makeShared<CTexture>();
std::string labelText;
if (**PSHOWNAMES && images[i].pWorkspace && !images[i].pWorkspace->m_name.empty())
labelText = images[i].pWorkspace->m_name;
else
labelText = std::to_string(images[i].workspaceID);
renderLabel(images[i].labelTex, labelText, LABEL_SIZE);
}
g_pHyprRenderer->m_bBlockSurfaceFeedback = false;
PMONITOR->m_activeSpecialWorkspace = openSpecial;
@ -189,10 +283,10 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
// zoom on the current workspace.
// const auto& TILE = images[std::clamp(currentid, 0, SIDE_LENGTH * SIDE_LENGTH)];
g_pAnimationManager->createAnimation(pMonitor->m_size * pMonitor->m_size / tileSize, size, g_pConfigManager->getAnimationPropertyConfig("windowsMove"), AVARDAMAGE_NONE);
g_pAnimationManager->createAnimation((-((pMonitor->m_size / (double)SIDE_LENGTH) * Vector2D{currentid % SIDE_LENGTH, currentid / SIDE_LENGTH}) * pMonitor->m_scale) *
(pMonitor->m_size / tileSize),
pos, g_pConfigManager->getAnimationPropertyConfig("windowsMove"), AVARDAMAGE_NONE);
Vector2D initSize = pMonitor->m_size * pMonitor->m_size / tileSize;
Vector2D initTilePos = tilePosForID(currentid, initSize, 0.0);
g_pAnimationManager->createAnimation(initSize, size, g_pConfigManager->getAnimationPropertyConfig("windowsMove"), AVARDAMAGE_NONE);
g_pAnimationManager->createAnimation(-(initTilePos * pMonitor->m_scale), pos, g_pConfigManager->getAnimationPropertyConfig("windowsMove"), AVARDAMAGE_NONE);
size->setUpdateCallback(damageMonitor);
pos->setUpdateCallback(damageMonitor);
@ -216,34 +310,184 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
info.cancelled = true;
lastMousePosLocal = g_pInputManager->getMouseCoordsInternal() - pMonitor->m_position;
// Check if we should start a drag
if (dragSourceID >= 0 && !dragging) {
Vector2D delta = lastMousePosLocal - dragStartPos;
if (delta.size() > 10.0)
dragging = true;
}
if (dragging)
damage();
};
auto onCursorSelect = [this](Event::SCallbackInfo& info) {
auto onMouseButton = [this](IPointer::SButtonEvent e, Event::SCallbackInfo& info) {
if (closing)
return;
info.cancelled = true;
static auto* const* PDRAGMOVE_ = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:enable_drag_move")->getDataStaticPtr();
if (**PDRAGMOVE_) {
if (e.state == WL_POINTER_BUTTON_STATE_PRESSED) {
// Record drag start
dragStartPos = lastMousePosLocal;
dragSourceID = -1;
dragging = false;
for (size_t j = 0; j < images.size(); ++j) {
const auto& box = images[j].box;
if (lastMousePosLocal.x >= box.x && lastMousePosLocal.x < box.x + box.w &&
lastMousePosLocal.y >= box.y && lastMousePosLocal.y < box.y + box.h) {
dragSourceID = j;
break;
}
}
} else {
// Button released
if (dragging) {
// Find target tile under cursor
int targetID = -1;
for (size_t j = 0; j < images.size(); ++j) {
const auto& box = images[j].box;
if (lastMousePosLocal.x >= box.x && lastMousePosLocal.x < box.x + box.w &&
lastMousePosLocal.y >= box.y && lastMousePosLocal.y < box.y + box.h) {
targetID = j;
break;
}
}
if (targetID >= 0 && targetID != dragSourceID && dragSourceID >= 0) {
auto srcWs = images[dragSourceID].pWorkspace;
auto dstWs = images[targetID].pWorkspace;
if (srcWs && dstWs) {
PHLWINDOW topWindow;
for (auto& w : g_pCompositor->m_windows) {
if (w->m_workspace == srcWs && !w->isHidden() && !w->isX11OverrideRedirect()) {
topWindow = w;
break;
}
}
if (topWindow) {
g_pCompositor->moveWindowToWorkspaceSafe(topWindow, dstWs);
redrawID(dragSourceID);
redrawID(targetID);
}
}
}
dragging = false;
dragSourceID = -1;
damage();
} else {
// Simple click - original behavior
dragging = false;
dragSourceID = -1;
selectHoveredWorkspace();
close();
}
}
} else {
// Drag disabled - original behavior on release only
if (e.state != WL_POINTER_BUTTON_STATE_PRESSED) {
selectHoveredWorkspace();
close();
}
}
};
auto onTouchSelect = [this](Event::SCallbackInfo& info) {
if (closing)
return;
info.cancelled = true;
selectHoveredWorkspace();
close();
};
mouseMoveHook = Event::bus()->m_events.input.mouse.move.listen([onCursorMove](Vector2D, Event::SCallbackInfo& info) { onCursorMove(info); });
touchMoveHook = Event::bus()->m_events.input.touch.motion.listen([onCursorMove](ITouch::SMotionEvent, Event::SCallbackInfo& info) { onCursorMove(info); });
mouseButtonHook = Event::bus()->m_events.input.mouse.button.listen([onCursorSelect](IPointer::SButtonEvent, Event::SCallbackInfo& info) { onCursorSelect(info); });
touchDownHook = Event::bus()->m_events.input.touch.down.listen([onCursorSelect](ITouch::SDownEvent, Event::SCallbackInfo& info) { onCursorSelect(info); });
mouseButtonHook = Event::bus()->m_events.input.mouse.button.listen(onMouseButton);
touchDownHook = Event::bus()->m_events.input.touch.down.listen([onTouchSelect](ITouch::SDownEvent, Event::SCallbackInfo& info) { onTouchSelect(info); });
static auto* const* PKBNAV = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:enable_keyboard_nav")->getDataStaticPtr();
if (**PKBNAV) {
selectedID = openedID;
keyPressHook = Event::bus()->m_events.input.keyboard.key.listen([this](IKeyboard::SKeyEvent keyEvent, Event::SCallbackInfo& info) {
if (closing)
return;
if (keyEvent.state != WL_KEYBOARD_KEY_STATE_PRESSED)
return;
auto keyboard = g_pSeatManager->m_keyboard.lock();
if (!keyboard)
return;
xkb_keysym_t sym = xkb_state_key_get_one_sym(keyboard->m_xkbState, keyEvent.keycode + 8);
int cols = SIDE_LENGTH;
int total = (int)images.size();
switch (sym) {
case XKB_KEY_Left:
if (selectedID > 0) selectedID--;
break;
case XKB_KEY_Right:
if (selectedID < total - 1) selectedID++;
break;
case XKB_KEY_Up:
if (selectedID - cols >= 0) selectedID -= cols;
break;
case XKB_KEY_Down:
if (selectedID + cols < total) selectedID += cols;
break;
case XKB_KEY_Return:
closeOnID = selectedID;
close();
break;
case XKB_KEY_Escape:
close();
break;
default:
if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) {
int idx = sym - XKB_KEY_1;
if (idx < total) {
closeOnID = idx;
close();
}
}
return;
}
info.cancelled = true;
damage();
});
}
}
void COverview::selectHoveredWorkspace() {
if (closing)
return;
// get tile x,y
int x = lastMousePosLocal.x / pMonitor->m_size.x * SIDE_LENGTH;
int y = lastMousePosLocal.y / pMonitor->m_size.y * SIDE_LENGTH;
closeOnID = x + y * SIDE_LENGTH;
// Check each tile's actual box for hit detection (handles centered partial rows)
for (size_t i = 0; i < images.size(); ++i) {
const auto& box = images[i].box;
if (lastMousePosLocal.x >= box.x && lastMousePosLocal.x < box.x + box.w &&
lastMousePosLocal.y >= box.y && lastMousePosLocal.y < box.y + box.h) {
closeOnID = i;
return;
}
}
// Fallback: use grid calculation and clamp to nearest valid tile
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
int x = lastMousePosLocal.x / pMonitor->m_size.x * cols;
int y = lastMousePosLocal.y / pMonitor->m_size.y * rows;
int idx = x + y * cols;
closeOnID = std::clamp(idx, 0, (int)images.size() - 1);
}
void COverview::redrawID(int id, bool forcelowres) {
@ -259,10 +503,13 @@ void COverview::redrawID(int id, bool forcelowres) {
g_pHyprRenderer->makeEGLCurrent();
id = std::clamp(id, 0, SIDE_LENGTH * SIDE_LENGTH);
id = std::clamp(id, 0, (int)images.size() - 1);
Vector2D tileSize = pMonitor->m_size / SIDE_LENGTH;
Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH, GAP_WIDTH} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {pMonitor->m_size.x / cols, pMonitor->m_size.y / rows};
Vector2D tileRenderSize = {(pMonitor->m_size.x - GAP_WIDTH * (cols - 1)) / cols,
(pMonitor->m_size.y - GAP_WIDTH * (rows - 1)) / rows};
CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2};
if (!forcelowres && (size->value() != pMonitor->m_size || closing))
@ -323,7 +570,7 @@ void COverview::redrawID(int id, bool forcelowres) {
void COverview::redrawAll(bool forcelowres) {
if (!pMonitor)
return;
for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
for (size_t i = 0; i < images.size(); ++i) {
redrawID(i, forcelowres);
}
}
@ -339,12 +586,12 @@ void COverview::onDamageReported() {
Vector2D SIZE = size->value();
Vector2D tileSize = (SIZE / SIDE_LENGTH);
Vector2D tileRenderSize = (SIZE - Vector2D{GAP_WIDTH, GAP_WIDTH} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
// const auto& TILE = images[std::clamp(openedID, 0, SIDE_LENGTH * SIDE_LENGTH)];
CBox texbox = CBox{(openedID % SIDE_LENGTH) * tileRenderSize.x + (openedID % SIDE_LENGTH) * GAP_WIDTH,
(openedID / SIDE_LENGTH) * tileRenderSize.y + (openedID / SIDE_LENGTH) * GAP_WIDTH, tileRenderSize.x, tileRenderSize.y}
.translate(pMonitor->m_position);
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {SIZE.x / cols, SIZE.y / rows};
Vector2D tileRenderSize = aspectCorrectTileSize(SIZE.x, SIZE.y, cols, rows, (double)GAP_WIDTH);
Vector2D tilePos = tilePosForID(openedID, SIZE, (double)GAP_WIDTH);
CBox texbox = CBox{tilePos.x, tilePos.y, tileRenderSize.x, tileRenderSize.y}.translate(pMonitor->m_position);
damage();
@ -354,21 +601,53 @@ void COverview::onDamageReported() {
g_pCompositor->scheduleFrameForMonitor(pMonitor.lock());
}
Vector2D COverview::tilePosForID(int id, Vector2D totalSize, double gapSize) const {
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileRenderSize = aspectCorrectTileSize(totalSize.x, totalSize.y, cols, rows, gapSize);
int tilesInLastRow = images.size() % cols;
if (tilesInLastRow == 0)
tilesInLastRow = cols;
int lastRow = ((int)images.size() - 1) / cols;
int actualRows = lastRow + 1;
double gridW = cols * tileRenderSize.x + (cols - 1) * gapSize;
double gridH = actualRows * tileRenderSize.y + (actualRows - 1) * gapSize;
double baseOffX = (totalSize.x - gridW) / 2.0;
double baseOffY = (totalSize.y - gridH) / 2.0;
int x = id % cols;
int y = id / cols;
double tileX;
if (y == lastRow && tilesInLastRow < cols) {
double rowW = tilesInLastRow * tileRenderSize.x + (tilesInLastRow - 1) * gapSize;
tileX = (totalSize.x - rowW) / 2.0 + x * (tileRenderSize.x + gapSize);
} else {
tileX = baseOffX + x * (tileRenderSize.x + gapSize);
}
double tileY = baseOffY + y * (tileRenderSize.y + gapSize);
return {tileX, tileY};
}
void COverview::close() {
if (closing)
return;
const int ID = closeOnID == -1 ? openedID : closeOnID;
const auto& TILE = images[std::clamp(ID, 0, SIDE_LENGTH * SIDE_LENGTH)];
const auto& TILE = images[std::clamp(ID, 0, (int)images.size() - 1)];
Vector2D tileSize = (pMonitor->m_size / SIDE_LENGTH);
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {pMonitor->m_size.x / cols, pMonitor->m_size.y / rows};
Vector2D targetSize = pMonitor->m_size * pMonitor->m_size / tileSize;
size->warp();
pos->warp();
*size = pMonitor->m_size * pMonitor->m_size / tileSize;
*pos = (-((pMonitor->m_size / (double)SIDE_LENGTH) * Vector2D{ID % SIDE_LENGTH, ID / SIDE_LENGTH}) * pMonitor->m_scale) * (pMonitor->m_size / tileSize);
*size = targetSize;
*pos = -(tilePosForID(ID, targetSize, 0.0) * pMonitor->m_scale);
closing = true;
@ -413,7 +692,7 @@ void COverview::onWorkspaceChange() {
else
startedOn = pMonitor->m_activeWorkspace;
for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) {
for (size_t i = 0; i < images.size(); ++i) {
if (images[i].workspaceID != pMonitor->activeWorkspaceID())
continue;
@ -440,22 +719,198 @@ void COverview::fullRender() {
Vector2D SIZE = size->value();
Vector2D tileSize = (SIZE / SIDE_LENGTH);
Vector2D tileRenderSize = (SIZE - Vector2D{GAPSIZE, GAPSIZE} * (SIDE_LENGTH - 1)) / SIDE_LENGTH;
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {SIZE.x / cols, SIZE.y / rows};
Vector2D tileRenderSize = aspectCorrectTileSize(SIZE.x, SIZE.y, cols, rows, GAPSIZE);
g_pHyprOpenGL->clear(BG_COLOR.stripA());
for (size_t y = 0; y < (size_t)SIDE_LENGTH; ++y) {
for (size_t x = 0; x < (size_t)SIDE_LENGTH; ++x) {
CBox texbox = {x * tileRenderSize.x + x * GAPSIZE, y * tileRenderSize.y + y * GAPSIZE, tileRenderSize.x, tileRenderSize.y};
texbox.scale(pMonitor->m_scale).translate(pos->value());
texbox.round();
CRegion damage{0, 0, INT16_MAX, INT16_MAX};
g_pHyprOpenGL->renderTextureInternal(images[x + y * SIDE_LENGTH].fb.getTexture(), texbox, {.damage = &damage, .a = 1.0});
for (size_t i = 0; i < images.size(); ++i) {
Vector2D tilePos = tilePosForID(i, SIZE, GAPSIZE);
CBox texbox = {tilePos.x, tilePos.y, tileRenderSize.x, tileRenderSize.y};
texbox.scale(pMonitor->m_scale).translate(pos->value());
texbox.round();
CRegion damage{0, 0, INT16_MAX, INT16_MAX};
// Staggered fade-in animation
static auto* const* PANIMATE_R = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:animate_entry")->getDataStaticPtr();
float tileAlpha = 1.0f;
if (**PANIMATE_R && !closing) {
auto elapsed = std::chrono::duration<float>(std::chrono::steady_clock::now() - createdAt).count();
float delay = i * 0.05f;
float duration = 0.2f;
if (elapsed < delay)
tileAlpha = 0.0f;
else if (elapsed < delay + duration)
tileAlpha = (elapsed - delay) / duration;
else
tileAlpha = 1.0f;
if (tileAlpha < 1.0f)
this->damage();
}
g_pHyprOpenGL->renderTextureInternal(images[i].fb.getTexture(), texbox, {.damage = &damage, .a = tileAlpha});
// Active workspace highlight
static auto* const* PACTIVECOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:active_highlight_col")->getDataStaticPtr();
static auto* const* PACTIVEBORDER = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:active_highlight_border")->getDataStaticPtr();
static auto* const* PHOVERCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:hover_highlight_col")->getDataStaticPtr();
static auto* const* PHOVERBORDER = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:hover_highlight_border")->getDataStaticPtr();
if (images[i].pWorkspace == startedOn) {
CHyprColor activeCol = **PACTIVECOL;
int borderW = **PACTIVEBORDER * pMonitor->m_scale;
CBox top = {texbox.x, texbox.y, texbox.w, borderW};
CBox bottom = {texbox.x, texbox.y + texbox.h - borderW, texbox.w, borderW};
CBox left = {texbox.x, texbox.y, borderW, texbox.h};
CBox right = {texbox.x + texbox.w - borderW, texbox.y, borderW, texbox.h};
g_pHyprOpenGL->renderRect(top, activeCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(bottom, activeCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(left, activeCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(right, activeCol, {.damage = &damage});
}
// Drag visual feedback: dim the source tile
if (dragging && dragSourceID == (int)i) {
CHyprColor dimCol{0.0f, 0.0f, 0.0f, 0.3f};
g_pHyprOpenGL->renderRect(texbox, dimCol, {.damage = &damage});
}
// Hover highlight (also used as drop target indicator during drag)
bool isHovered = (lastMousePosLocal.x >= images[i].box.x && lastMousePosLocal.x < images[i].box.x + images[i].box.w &&
lastMousePosLocal.y >= images[i].box.y && lastMousePosLocal.y < images[i].box.y + images[i].box.h);
if (isHovered && (dragging ? (int)i != dragSourceID : images[i].pWorkspace != startedOn)) {
CHyprColor hoverCol = **PHOVERCOL;
int borderW = **PHOVERBORDER * pMonitor->m_scale;
CBox top = {texbox.x, texbox.y, texbox.w, borderW};
CBox bottom = {texbox.x, texbox.y + texbox.h - borderW, texbox.w, borderW};
CBox left = {texbox.x, texbox.y, borderW, texbox.h};
CBox right = {texbox.x + texbox.w - borderW, texbox.y, borderW, texbox.h};
g_pHyprOpenGL->renderRect(top, hoverCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(bottom, hoverCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(left, hoverCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(right, hoverCol, {.damage = &damage});
}
// Keyboard selection indicator
if (selectedID >= 0 && selectedID == (int)i) {
CHyprColor selCol{1.0f, 1.0f, 1.0f, 0.9f};
int selBorderW = 3 * pMonitor->m_scale;
CBox top = {texbox.x, texbox.y, texbox.w, selBorderW};
CBox bottom = {texbox.x, texbox.y + texbox.h - selBorderW, texbox.w, selBorderW};
CBox left = {texbox.x, texbox.y, selBorderW, texbox.h};
CBox right = {texbox.x + texbox.w - selBorderW, texbox.y, selBorderW, texbox.h};
g_pHyprOpenGL->renderRect(top, selCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(bottom, selCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(left, selCol, {.damage = &damage});
g_pHyprOpenGL->renderRect(right, selCol, {.damage = &damage});
}
// Render workspace label
if (images[i].labelTex && images[i].labelTex->m_texID) {
static auto* const* PLABELSIZE2 = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_size")->getDataStaticPtr();
static auto const* PLABELPOS = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_pos")->getDataStaticPtr();
const float labelSize = **PLABELSIZE2 * pMonitor->m_scale;
const float padding = 8 * pMonitor->m_scale;
const std::string posStr = *PLABELPOS;
float lx, ly;
if (posStr == "top_left") {
lx = texbox.x + padding;
ly = texbox.y + padding;
} else if (posStr == "bottom_right") {
lx = texbox.x + texbox.w - labelSize - padding;
ly = texbox.y + texbox.h - labelSize - padding;
} else if (posStr == "bottom_left") {
lx = texbox.x + padding;
ly = texbox.y + texbox.h - labelSize - padding;
} else { // "top_right" (default)
lx = texbox.x + texbox.w - labelSize - padding;
ly = texbox.y + padding;
}
CBox labelBox = {lx, ly, labelSize, labelSize};
labelBox.round();
g_pHyprOpenGL->renderTextureInternal(images[i].labelTex, labelBox, {.damage = &damage, .a = 0.9f * tileAlpha});
}
}
}
void COverview::renderLabel(SP<CTexture>& tex, const std::string& text, int size) {
if (!tex)
return;
const auto PMONITOR = pMonitor.lock();
if (!PMONITOR)
return;
const float scale = PMONITOR->m_scale;
const int bufferSize = std::max(1, (int)(size * scale));
const auto CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bufferSize, bufferSize);
if (cairo_surface_status(CAIROSURFACE) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy(CAIROSURFACE);
return;
}
const auto CAIRO = cairo_create(CAIROSURFACE);
// Clear the surface
cairo_save(CAIRO);
cairo_set_operator(CAIRO, CAIRO_OPERATOR_CLEAR);
cairo_paint(CAIRO);
cairo_restore(CAIRO);
// Draw semi-transparent dark background circle
cairo_set_source_rgba(CAIRO, 0.0, 0.0, 0.0, 0.7);
cairo_arc(CAIRO, bufferSize / 2.0, bufferSize / 2.0, bufferSize / 2.0 - 2, 0, 2 * M_PI);
cairo_fill(CAIRO);
// Draw text using Pango
PangoLayout* layout = pango_cairo_create_layout(CAIRO);
pango_layout_set_text(layout, text.c_str(), -1);
PangoFontDescription* fontDesc = pango_font_description_from_string("sans bold");
pango_font_description_set_size(fontDesc, (int)(size * 0.5 * scale * PANGO_SCALE));
pango_layout_set_font_description(layout, fontDesc);
pango_font_description_free(fontDesc);
static auto* const* PLABELCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_col")->getDataStaticPtr();
CHyprColor labelColor = **PLABELCOL;
cairo_set_source_rgba(CAIRO, labelColor.r, labelColor.g, labelColor.b, labelColor.a);
PangoRectangle ink_rect, logical_rect;
pango_layout_get_extents(layout, &ink_rect, &logical_rect);
const double xOffset = (bufferSize / 2.0 - logical_rect.width / PANGO_SCALE / 2.0);
const double yOffset = (bufferSize / 2.0 - logical_rect.height / PANGO_SCALE / 2.0);
cairo_move_to(CAIRO, xOffset, yOffset);
pango_cairo_show_layout(CAIRO, layout);
g_object_unref(layout);
cairo_surface_flush(CAIROSURFACE);
// Upload to OpenGL texture
const auto DATA = cairo_image_surface_get_data(CAIROSURFACE);
tex->allocate();
glBindTexture(GL_TEXTURE_2D, tex->m_texID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
#ifndef GLES2
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
#endif
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bufferSize, bufferSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
cairo_destroy(CAIRO);
cairo_surface_destroy(CAIROSURFACE);
}
static float lerp(const float& from, const float& to, const float perc) {
return (to - from) * perc + from;
}
@ -480,11 +935,12 @@ void COverview::onSwipeUpdate(double delta) {
const float PERC = closing ? std::clamp(delta / (double)**PDISTANCE, 0.0, 1.0) : 1.0 - std::clamp(delta / (double)**PDISTANCE, 0.0, 1.0);
const auto WORKSPACE_FOCUS_ID = closing && closeOnID != -1 ? closeOnID : openedID;
Vector2D tileSize = (pMonitor->m_size / SIDE_LENGTH);
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {pMonitor->m_size.x / cols, pMonitor->m_size.y / rows};
const auto SIZEMAX = pMonitor->m_size * pMonitor->m_size / tileSize;
const auto POSMAX = (-((pMonitor->m_size / (double)SIDE_LENGTH) * Vector2D{WORKSPACE_FOCUS_ID % SIDE_LENGTH, WORKSPACE_FOCUS_ID / SIDE_LENGTH}) * pMonitor->m_scale) *
(pMonitor->m_size / tileSize);
const auto POSMAX = -(tilePosForID(WORKSPACE_FOCUS_ID, SIZEMAX, 0.0) * pMonitor->m_scale);
const auto SIZEMIN = pMonitor->m_size;
const auto POSMIN = Vector2D{0, 0};
@ -497,8 +953,11 @@ void COverview::onSwipeEnd() {
if (closing || !m_isSwiping)
return;
int cols = SIDE_LENGTH;
int rows = dynamicGrid ? gridRows : SIDE_LENGTH;
Vector2D tileSize = {pMonitor->m_size.x / cols, pMonitor->m_size.y / rows};
const auto SIZEMIN = pMonitor->m_size;
const auto SIZEMAX = pMonitor->m_size * pMonitor->m_size / (pMonitor->m_size / SIDE_LENGTH);
const auto SIZEMAX = pMonitor->m_size * pMonitor->m_size / tileSize;
const auto PERC = (size->value() - SIZEMIN).x / (SIZEMAX - SIZEMIN).x;
if (PERC > 0.5) {
close();
@ -507,7 +966,7 @@ void COverview::onSwipeEnd() {
*size = pMonitor->m_size;
*pos = {0, 0};
size->setCallbackOnEnd([this](WP<Hyprutils::Animation::CBaseAnimatedVariable> thisptr) { redrawAll(true); });
size->setCallbackOnEnd([this](auto) { redrawAll(true); });
swipeWasCommenced = false;
m_isSwiping = false;

View file

@ -5,8 +5,10 @@
#include "globals.hpp"
#include <hyprland/src/desktop/DesktopTypes.hpp>
#include <hyprland/src/render/Framebuffer.hpp>
#include <hyprland/src/render/Texture.hpp>
#include <hyprland/src/helpers/AnimatedVariable.hpp>
#include <hyprland/src/event/EventBus.hpp>
#include <chrono>
#include <vector>
// saves on resources, but is a bit broken rn with blur.
@ -46,15 +48,20 @@ class COverview {
void redrawAll(bool forcelowres = false);
void onWorkspaceChange();
void fullRender();
void renderLabel(SP<CTexture>& tex, const std::string& text, int size);
Vector2D tilePosForID(int id, Vector2D totalSize, double gapSize) const;
int SIDE_LENGTH = 3;
int GAP_WIDTH = 5;
CHyprColor BG_COLOR = CHyprColor{0.1, 0.1, 0.1, 1.0};
int SIDE_LENGTH = 3; // columns in grid
int GAP_WIDTH = 5;
CHyprColor BG_COLOR = CHyprColor{0.1, 0.1, 0.1, 1.0};
int gridRows = 3; // rows in dynamic grid
bool dynamicGrid = false;
bool damageDirty = false;
struct SWorkspaceImage {
CFramebuffer fb;
SP<CTexture> labelTex;
int64_t workspaceID = -1;
PHLWORKSPACE pWorkspace;
CBox box;
@ -82,6 +89,15 @@ class COverview {
bool swipe = false;
bool swipeWasCommenced = false;
int hoveredID = -1;
int selectedID = -1;
CHyprSignalListener keyPressHook;
bool dragging = false;
int dragSourceID = -1;
Vector2D dragStartPos;
std::chrono::steady_clock::time_point createdAt;
friend class COverviewPassElement;
};