diff --git a/.gitignore b/.gitignore index 00436c6..19d3638 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ result result-man build/ -.cache/ \ No newline at end of file +.cache/ + +# Claude Code +CLAUDE.md \ No newline at end of file diff --git a/hyprexpo/main.cpp b/hyprexpo/main.cpp index 1f74f38..cfa2791 100644 --- a/hyprexpo/main.cpp +++ b/hyprexpo/main.cpp @@ -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"}; diff --git a/hyprexpo/overview.cpp b/hyprexpo/overview.cpp index 00bb8c9..1cae0f9 100644 --- a/hyprexpo/overview.cpp +++ b/hyprexpo/overview.cpp @@ -1,5 +1,10 @@ #include "overview.hpp" #include +#include +#include +#include +#include +#include #define private public #include #include @@ -10,11 +15,24 @@ #include #include #include +#include #include #include #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 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 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(); + 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(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& 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 thisptr) { redrawAll(true); }); + size->setCallbackOnEnd([this](auto) { redrawAll(true); }); swipeWasCommenced = false; m_isSwiping = false; diff --git a/hyprexpo/overview.hpp b/hyprexpo/overview.hpp index 144add9..13a4758 100644 --- a/hyprexpo/overview.hpp +++ b/hyprexpo/overview.hpp @@ -5,8 +5,10 @@ #include "globals.hpp" #include #include +#include #include #include +#include #include // 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& 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 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; };