From 54ec6d630ee964b761e994373ceff1d2ab74b230 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Apr 2026 15:00:54 -0700 Subject: [PATCH] Restore custom hyprexpo overview behavior --- hyprexpo/README.md | 3 + hyprexpo/main.cpp | 52 ++++++++++++++++++ hyprexpo/overview.cpp | 124 ++++++++++++++++++++++++++++++++++++++++-- hyprexpo/overview.hpp | 11 +++- 4 files changed, 181 insertions(+), 9 deletions(-) diff --git a/hyprexpo/README.md b/hyprexpo/README.md index 09267df..084f02b 100644 --- a/hyprexpo/README.md +++ b/hyprexpo/README.md @@ -28,6 +28,8 @@ gap_size | number | gap between desktops | `5` bg_col | color | color in gaps (between desktops) | `rgb(000000)` workspace_method | [center/first] [workspace] | position of the desktops | `center current` skip_empty | boolean | whether the grid displays workspaces sequentially by id using selector "r" (`false`) or skips empty workspaces using selector "m" (`true`) | `false` +show_workspace_numbers | boolean | show numeric labels for workspaces | `false` +workspace_number_color | color | color of workspace number labels | `rgb(ffffff)` gesture_distance | number | how far is the max for the gesture | `300` ### Keywords @@ -53,6 +55,7 @@ Here are a list of options you can use: | --- | --- | toggle | displays if hidden, hide if displayed select | selects the hovered desktop +bring | brings a window from the hovered desktop to the current desktop off | hides the overview disable | same as `off` on | displays the overview diff --git a/hyprexpo/main.cpp b/hyprexpo/main.cpp index 32c1c02..9e86ffb 100644 --- a/hyprexpo/main.cpp +++ b/hyprexpo/main.cpp @@ -74,6 +74,47 @@ static void hkAddDamageB(void* thisptr, const pixman_region32_t* rg) { g_pOverview->onDamageReported(); } +static PHLWINDOW windowToBringFromWorkspace(const PHLWORKSPACE& workspace) { + if (!workspace) + return nullptr; + + for (auto it = g_pCompositor->m_windows.rbegin(); it != g_pCompositor->m_windows.rend(); ++it) { + const auto& w = *it; + if (!w || w->m_workspace != workspace || !w->m_isMapped || w->isHidden()) + continue; + + return w; + } + + return nullptr; +} + +static SDispatchResult bringWindowFromWorkspace(int64_t sourceWorkspaceID) { + if (sourceWorkspaceID == WORKSPACE_INVALID) + return {.success = false, .error = "selected workspace is empty"}; + + const auto FOCUSSTATE = Desktop::focusState(); + const auto MONITOR = FOCUSSTATE->monitor(); + if (!MONITOR || !MONITOR->m_activeWorkspace) + return {.success = false, .error = "no active monitor/workspace"}; + + if (sourceWorkspaceID == MONITOR->activeWorkspaceID()) + return {}; + + const auto SOURCEWORKSPACE = g_pCompositor->getWorkspaceByID(sourceWorkspaceID); + if (!SOURCEWORKSPACE) + return {.success = false, .error = "selected workspace is not open"}; + + const auto WINDOW = windowToBringFromWorkspace(SOURCEWORKSPACE); + if (!WINDOW) + return {.success = false, .error = "selected workspace has no mapped windows"}; + + g_pCompositor->moveWindowToWorkspaceSafe(WINDOW, MONITOR->m_activeWorkspace); + FOCUSSTATE->fullWindowFocus(WINDOW, Desktop::FOCUS_REASON_KEYBIND); + g_pCompositor->warpCursorTo(WINDOW->middle()); + return {}; +} + static SDispatchResult onExpoDispatcher(std::string arg) { if (g_pOverview && g_pOverview->m_isSwiping) @@ -86,6 +127,15 @@ static SDispatchResult onExpoDispatcher(std::string arg) { } return {}; } + if (arg == "bring") { + if (g_pOverview) { + g_pOverview->selectHoveredWorkspace(); + const auto BRINGRESULT = bringWindowFromWorkspace(g_pOverview->selectedWorkspaceID()); + g_pOverview->close(false); + return BRINGRESULT; + } + return {}; + } if (arg == "toggle") { if (g_pOverview) g_pOverview->close(); @@ -264,6 +314,8 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:bg_col", "background color", 0xFF111111)); HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:workspace_method", "workspace method", "center current")); HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:skip_empty", "skip empty workspaces", 0)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:show_workspace_numbers", "show workspace numbers", 0)); + HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:workspace_number_color", "workspace number color", 0xFFFFFFFF)); HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprexpo:gesture_distance", "gesture distance", 200)); HyprlandAPI::reloadConfig(); diff --git a/hyprexpo/overview.cpp b/hyprexpo/overview.cpp index f21a81e..7ba0ec8 100644 --- a/hyprexpo/overview.cpp +++ b/hyprexpo/overview.cpp @@ -1,6 +1,8 @@ #include "overview.hpp" #include #include +#include +#include #define private public #define protected public #include @@ -28,6 +30,8 @@ static const CConfigValue PCOLUMNS("plugin:hyprexpo:columns"); static const CConfigValue PGAPS("plugin:hyprexpo:gap_size"); static const CConfigValue PCOL("plugin:hyprexpo:bg_col"); static const CConfigValue PSKIP("plugin:hyprexpo:skip_empty"); +static const CConfigValue PSHOWNUM("plugin:hyprexpo:show_workspace_numbers"); +static const CConfigValue PNUMCOL("plugin:hyprexpo:workspace_number_color"); static const CConfigValue PMETHOD("plugin:hyprexpo:workspace_method"); static const CConfigValue PDISTANCE("plugin:hyprexpo:gesture_distance"); @@ -46,6 +50,84 @@ static void ensureFramebuffer(COverview::SWorkspaceImage& image, const CBox& mon } } +static Vector2D renderLabelTexture(SP out, const std::string& text, const CHyprColor& color, int fontSizePx) { + if (!out || text.empty() || fontSizePx <= 0) + return {}; + + auto measureSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + auto measureCairo = cairo_create(measureSurface); + + PangoLayout* measureLayout = pango_cairo_create_layout(measureCairo); + pango_layout_set_text(measureLayout, text.c_str(), -1); + auto* fontDesc = pango_font_description_from_string("Sans Bold"); + pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE); + pango_layout_set_font_description(measureLayout, fontDesc); + pango_font_description_free(fontDesc); + + PangoRectangle inkRect, logicalRect; + pango_layout_get_extents(measureLayout, &inkRect, &logicalRect); + + const int textW = std::max(1, (int)std::ceil(logicalRect.width / (double)PANGO_SCALE)); + const int textH = std::max(1, (int)std::ceil(logicalRect.height / (double)PANGO_SCALE)); + + g_object_unref(measureLayout); + cairo_destroy(measureCairo); + cairo_surface_destroy(measureSurface); + + const int pad = std::max(4, (int)std::round(fontSizePx * 0.35)); + const int width = textW + pad * 2; + const int height = textH + pad * 2; + + auto surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + auto cairo = cairo_create(surface); + + cairo_save(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(cairo); + cairo_restore(cairo); + + cairo_set_source_rgba(cairo, 0.0, 0.0, 0.0, 0.55); + cairo_rectangle(cairo, 0, 0, width, height); + cairo_fill(cairo); + + PangoLayout* layout = pango_cairo_create_layout(cairo); + pango_layout_set_text(layout, text.c_str(), -1); + fontDesc = pango_font_description_from_string("Sans Bold"); + pango_font_description_set_size(fontDesc, fontSizePx * PANGO_SCALE); + pango_layout_set_font_description(layout, fontDesc); + pango_font_description_free(fontDesc); + + pango_layout_get_extents(layout, &inkRect, &logicalRect); + const double xOffset = (width - logicalRect.width / (double)PANGO_SCALE) / 2.0; + const double yOffset = (height - logicalRect.height / (double)PANGO_SCALE) / 2.0; + + cairo_set_source_rgba(cairo, color.r, color.g, color.b, color.a); + cairo_move_to(cairo, xOffset, yOffset); + pango_cairo_show_layout(cairo, layout); + + g_object_unref(layout); + cairo_surface_flush(surface); + + const auto DATA = cairo_image_surface_get_data(surface); + out->allocate({width, height}); + out->bind(); + out->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST); + out->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST); + +#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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA); + out->unbind(); + + cairo_destroy(cairo); + cairo_surface_destroy(surface); + + return {width, height}; +} + static void damageMonitor(WP thisptr) { g_pOverview->damage(); } @@ -62,9 +144,10 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn const auto PMONITOR = Desktop::focusState()->monitor(); pMonitor = PMONITOR; - SIDE_LENGTH = *PCOLUMNS; - GAP_WIDTH = *PGAPS; - BG_COLOR = CHyprColor(*PCOL); + SIDE_LENGTH = *PCOLUMNS; + GAP_WIDTH = *PGAPS; + BG_COLOR = CHyprColor(*PCOL); + showWorkspaceNumbers = *PSHOWNUM; // process the method bool methodCenter = true; @@ -152,6 +235,17 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn Vector2D tileRenderSize = (pMonitor->m_size - Vector2D{GAP_WIDTH * pMonitor->m_scale, GAP_WIDTH * pMonitor->m_scale} * (SIDE_LENGTH - 1)) / SIDE_LENGTH; CBox monbox{0, 0, tileSize.x * 2, tileSize.y * 2}; + if (showWorkspaceNumbers) { + const CHyprColor numberColor = CHyprColor(*PNUMCOL); + const int fontSizePx = std::max(12, (int)std::round(tileRenderSize.y * pMonitor->m_scale * 0.22)); + for (auto& image : images) { + if (image.workspaceID == WORKSPACE_INVALID) + continue; + image.labelTex = g_pHyprRenderer->createTexture(false); + image.labelSizePx = renderLabelTexture(image.labelTex, std::to_string(image.workspaceID), numberColor, fontSizePx); + } + } + if (!ENABLE_LOWRES) monbox = {{0, 0}, pMonitor->m_pixelSize}; @@ -377,7 +471,15 @@ void COverview::onDamageReported() { g_pCompositor->scheduleFrameForMonitor(pMonitor.lock()); } -void COverview::close() { +int64_t COverview::selectedWorkspaceID() const { + const int ID = closeOnID == -1 ? openedID : closeOnID; + if (ID < 0 || ID >= (int)images.size()) + return WORKSPACE_INVALID; + + return images[ID].workspaceID; +} + +void COverview::close(bool switchToSelection) { if (closing) return; @@ -397,7 +499,7 @@ void COverview::close() { redrawAll(); - if (TILE.workspaceID != pMonitor->activeWorkspaceID()) { + if (switchToSelection && TILE.workspaceID != pMonitor->activeWorkspaceID()) { pMonitor->setSpecialWorkspace(0); // If this tile's workspace was WORKSPACE_INVALID, move to the next @@ -474,7 +576,17 @@ void COverview::fullRender() { texbox.scale(pMonitor->m_scale).translate(pos->value()); texbox.round(); CRegion damage{0, 0, INT16_MAX, INT16_MAX}; - Render::GL::g_pHyprOpenGL->renderTextureInternal(images[x + y * SIDE_LENGTH].fb->getTexture(), texbox, {.damage = &damage, .a = 1.0}); + auto& image = images[x + y * SIDE_LENGTH]; + Render::GL::g_pHyprOpenGL->renderTextureInternal(image.fb->getTexture(), texbox, {.damage = &damage, .a = 1.0}); + + if (showWorkspaceNumbers && image.workspaceID != WORKSPACE_INVALID && image.labelTex && image.labelTex->ok() && image.labelSizePx.x > 0 && image.labelSizePx.y > 0) { + const Vector2D labelSize = image.labelSizePx / pMonitor->m_scale; + const float margin = std::max(4.0, tileRenderSize.y * 0.05); + CBox labelBox = {x * tileRenderSize.x + x * GAPSIZE + margin, y * tileRenderSize.y + y * GAPSIZE + margin, labelSize.x, labelSize.y}; + labelBox.scale(pMonitor->m_scale).translate(pos->value()); + labelBox.round(); + Render::GL::g_pHyprOpenGL->renderTexture(image.labelTex, labelBox, {.a = 1.0}); + } } } } diff --git a/hyprexpo/overview.hpp b/hyprexpo/overview.hpp index 799df0b..ecad6c5 100644 --- a/hyprexpo/overview.hpp +++ b/hyprexpo/overview.hpp @@ -5,6 +5,7 @@ #include "globals.hpp" #include #include +#include #include #include #include @@ -32,8 +33,9 @@ class COverview { void onSwipeEnd(); // close without a selection - void close(); + void close(bool switchToSelection = true); void selectHoveredWorkspace(); + int64_t selectedWorkspaceID() const; bool blockOverviewRendering = false; bool blockDamageReporting = false; @@ -46,6 +48,8 @@ class COverview { int64_t workspaceID = -1; PHLWORKSPACE pWorkspace; CBox box; + SP labelTex; + Vector2D labelSizePx; }; private: @@ -79,8 +83,9 @@ class COverview { CHyprSignalListener touchMoveHook; CHyprSignalListener touchDownHook; - bool swipe = false; - bool swipeWasCommenced = false; + bool swipe = false; + bool swipeWasCommenced = false; + bool showWorkspaceNumbers = false; friend class COverviewPassElement; };