diff --git a/hyprexpo/README.md b/hyprexpo/README.md index 97bd1d4..6a740b3 100644 --- a/hyprexpo/README.md +++ b/hyprexpo/README.md @@ -13,6 +13,7 @@ plugin { gap_size = 5 bg_col = rgb(111111) workspace_method = center current # [center/first] [workspace] e.g. first 1 or center m+1 + show_label = true gesture_distance = 300 # how far is the "max" for the gesture } @@ -28,6 +29,9 @@ 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_label | boolean | whether to show workspace name labels | `false` +label_font_size | number | font size for workspace labels (minimum 8) | `24` +label_anchor | string | label position: `tl`, `tr`, `bl`, `br`, `tc`, `bc`, `cl`, `cr`, `cc` (top-left, top-right, bottom-left, bottom-right, top-center, bottom-center, center-left, center-right, center) | `tl` gesture_distance | number | how far is the max for the gesture | `300` ### Keywords diff --git a/hyprexpo/main.cpp b/hyprexpo/main.cpp index 1f74f38..f59d031 100644 --- a/hyprexpo/main.cpp +++ b/hyprexpo/main.cpp @@ -250,6 +250,9 @@ 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:show_label", Hyprlang::INT{0}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size", Hyprlang::INT{24}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_anchor", Hyprlang::STRING{"tl"}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:gesture_distance", Hyprlang::INT{200}); diff --git a/hyprexpo/overview.cpp b/hyprexpo/overview.cpp index 00bb8c9..3eab65a 100644 --- a/hyprexpo/overview.cpp +++ b/hyprexpo/overview.cpp @@ -14,6 +14,7 @@ #include #undef private #include "OverviewPassElement.hpp" +#include static void damageMonitor(WP thisptr) { g_pOverview->damage(); @@ -30,15 +31,22 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn 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* PMETHOD = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:workspace_method")->getDataStaticPtr(); + static auto* const* PSHOWLABEL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:show_label")->getDataStaticPtr(); + static auto* const* PFONTSIZE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size")->getDataStaticPtr(); + static auto const* PANCHOR = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_anchor")->getDataStaticPtr(); SIDE_LENGTH = **PCOLUMNS; GAP_WIDTH = **PGAPS; BG_COLOR = **PCOL; + show_label = **PSHOWLABEL; + fontSize = std::max(8, (int)**PFONTSIZE); + + labelAnchor = parseLabelAnchor(*PANCHOR); // process the method bool methodCenter = true; @@ -142,6 +150,7 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn for (size_t i = 0; i < (size_t)(SIDE_LENGTH * SIDE_LENGTH); ++i) { COverview::SWorkspaceImage& image = images[i]; image.fb.alloc(monbox.w, monbox.h, PMONITOR->m_output->state->state().drmFormat); + image.textTex = makeShared(); CRegion fakeDamage{0, 0, INT16_MAX, INT16_MAX}; g_pHyprRenderer->beginRender(PMONITOR, fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &image.fb); @@ -430,6 +439,140 @@ void COverview::render() { g_pHyprRenderer->m_renderPass.add(makeUnique()); } +COverview::LabelAnchor COverview::parseLabelAnchor(const std::string& anchorStr) { + if (anchorStr == "tl") { + return LabelAnchor::TOP_LEFT; + } else if (anchorStr == "tr") { + return LabelAnchor::TOP_RIGHT; + } else if (anchorStr == "bl") { + return LabelAnchor::BOTTOM_LEFT; + } else if (anchorStr == "br") { + return LabelAnchor::BOTTOM_RIGHT; + } else if (anchorStr == "tc") { + return LabelAnchor::TOP_CENTER; + } else if (anchorStr == "bc") { + return LabelAnchor::BOTTOM_CENTER; + } else if (anchorStr == "cl") { + return LabelAnchor::CENTER_LEFT; + } else if (anchorStr == "cr") { + return LabelAnchor::CENTER_RIGHT; + } else if (anchorStr == "cc") { + return LabelAnchor::CENTER; + } else { + return LabelAnchor::TOP_LEFT; // default fallback + } +} + +Vector2D COverview::calculateTextPosition(const CBox& texbox, const Vector2D& textBufferSize, const double padding, LabelAnchor anchor) { + switch (anchor) { + case LabelAnchor::TOP_LEFT: + return Vector2D{texbox.x + padding, texbox.y + padding}; + case LabelAnchor::TOP_RIGHT: + return Vector2D{texbox.x + texbox.width - textBufferSize.x - padding, texbox.y + padding}; + case LabelAnchor::BOTTOM_LEFT: + return Vector2D{texbox.x + padding, texbox.y + texbox.height - textBufferSize.y - padding}; + case LabelAnchor::BOTTOM_RIGHT: + return Vector2D{texbox.x + texbox.width - textBufferSize.x - padding, texbox.y + texbox.height - textBufferSize.y - padding}; + case LabelAnchor::TOP_CENTER: + return Vector2D{texbox.x + texbox.width / 2.0 - textBufferSize.x / 2.0, texbox.y + padding}; + case LabelAnchor::BOTTOM_CENTER: + return Vector2D{texbox.x + texbox.width / 2.0 - textBufferSize.x / 2.0, texbox.y + texbox.height - textBufferSize.y - padding}; + case LabelAnchor::CENTER_LEFT: + return Vector2D{texbox.x + padding, texbox.y + texbox.height / 2.0 - textBufferSize.y / 2.0}; + case LabelAnchor::CENTER_RIGHT: + return Vector2D{texbox.x + texbox.width - textBufferSize.x - padding, texbox.y + texbox.height / 2.0 - textBufferSize.y / 2.0}; + case LabelAnchor::CENTER: + return Vector2D{texbox.x + texbox.width / 2.0 - textBufferSize.x / 2.0, texbox.y + texbox.height / 2.0 - textBufferSize.y / 2.0}; + default: + return Vector2D{texbox.x + padding, texbox.y + padding}; // fallback to TOP_LEFT + } +} + +double COverview::calculateTextWidth(const std::string& text, const float scale, const int fontSize) { + // Create a temporary surface for measurement and Pango layout + const auto CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + const auto CAIRO = cairo_create(CAIROSURFACE); + PangoLayout* layout = pango_cairo_create_layout(CAIRO); + pango_layout_set_text(layout, text.c_str(), -1); + + PangoFontDescription* fontDesc = pango_font_description_from_string("sans"); + pango_font_description_set_size(fontDesc, fontSize * scale * PANGO_SCALE); + pango_font_description_set_weight(fontDesc, PANGO_WEIGHT_BOLD); + pango_layout_set_font_description(layout, fontDesc); + PangoRectangle ink_rect, logical_rect; + pango_layout_get_extents(layout, &ink_rect, &logical_rect); + + // Clean up + pango_font_description_free(fontDesc); + g_object_unref(layout); + cairo_destroy(CAIRO); + cairo_surface_destroy(CAIROSURFACE); + + return (double)logical_rect.width / PANGO_SCALE; +} + +void COverview::renderText(SP out, const std::string& text, const CHyprColor& color, const Vector2D& bufferSize, const float scale, const int fontSize) { + const auto CAIROSURFACE = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, bufferSize.x, bufferSize.y); + const auto CAIRO = cairo_create(CAIROSURFACE); + + // clear the pixmap + cairo_save(CAIRO); + cairo_set_operator(CAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(CAIRO); + cairo_restore(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"); + pango_font_description_set_size(fontDesc, fontSize * scale * PANGO_SCALE); + pango_font_description_set_weight(fontDesc, PANGO_WEIGHT_BOLD); + pango_layout_set_font_description(layout, fontDesc); + pango_font_description_free(fontDesc); + + const int maxWidth = bufferSize.x; + + pango_layout_set_width(layout, maxWidth * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE); + + cairo_set_source_rgba(CAIRO, color.r, color.g, color.b, color.a); + + PangoRectangle ink_rect, logical_rect; + pango_layout_get_extents(layout, &ink_rect, &logical_rect); + + const int layoutWidth = ink_rect.width; + const int layoutHeight = logical_rect.height; + + const double xOffset = (bufferSize.x / 2.0 - layoutWidth / PANGO_SCALE / 2.0); + const double yOffset = (bufferSize.y / 2.0 - layoutHeight / PANGO_SCALE / 2.0); + + cairo_move_to(CAIRO, xOffset, yOffset); + pango_cairo_show_layout(CAIRO, layout); + + g_object_unref(layout); + + cairo_surface_flush(CAIROSURFACE); + + // copy the data to an OpenGL texture we have + const auto DATA = cairo_image_surface_get_data(CAIROSURFACE); + out->allocate(); + glBindTexture(GL_TEXTURE_2D, out->m_texID); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, 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, bufferSize.x, bufferSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA); + + // delete cairo + cairo_destroy(CAIRO); + cairo_surface_destroy(CAIROSURFACE); +} + void COverview::fullRender() { const auto GAPSIZE = (closing ? (1.0 - size->getPercent()) : size->getPercent()) * GAP_WIDTH; @@ -447,11 +590,55 @@ void COverview::fullRender() { 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}; + const auto idx = x + y * SIDE_LENGTH; + 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}); + g_pHyprOpenGL->renderTextureInternal(images[idx].fb.getTexture(), texbox, {.damage = &damage, .a = 1.0}); + + // Render workspace name in top left corner (if enabled) + const auto& image = images[idx]; + if (image.workspaceID != WORKSPACE_INVALID && image.pWorkspace && show_label) { + // Generate text for workspace name and remove "name:" prefix if present + std::string workspaceName = image.pWorkspace->getConfigName(); + if (workspaceName.starts_with("name:")) { + workspaceName = workspaceName.substr(5); // Remove "name:" prefix + } + + // Trim workspace name to max 30 characters + if (workspaceName.length() > 30) { + workspaceName = workspaceName.substr(0, 27) + "..."; + } + + // Text rendering parameters using configurable font size + const CHyprColor textColor = CHyprColor{1.0, 1.0, 1.0, 1.0}; // Solid white text + + // Calculate accurate text dimensions using Pango + const double textWidthRaw = calculateTextWidth(workspaceName, pMonitor->m_scale, fontSize); + // Add padding and limit to max 70% of tile width + const double textWidth = std::min(tileRenderSize.x * 0.7, textWidthRaw + 20.0); + // Height scales with font size (1.5x font size + padding) + const double textHeight = fontSize * 1.5 + 10.0; + const Vector2D textBufferSize = Vector2D{textWidth, textHeight}; + + // Calculate text position based on anchor + const double padding = 10.0; + Vector2D textPos = calculateTextPosition(texbox, textBufferSize, padding, labelAnchor); + + if (image.textTex->m_texID == 0) { + renderText(image.textTex, workspaceName, textColor, textBufferSize, pMonitor->m_scale, fontSize); + } + + // Render semi-transparent dark background behind text + const CHyprColor bgColor = CHyprColor{0.0, 0.0, 0.0, 0.7}; + CBox bgBox = {textPos.x - 5.0, textPos.y - 3.0, textBufferSize.x + 10.0, textBufferSize.y + 6.0}; + g_pHyprOpenGL->renderRect(bgBox, bgColor, {.round = 0}); + + // Render the text + CBox textBox = {textPos.x, textPos.y, textBufferSize.x, textBufferSize.y}; + g_pHyprOpenGL->renderTexture(image.textTex, textBox, {.a = 1.0}); + } } } } diff --git a/hyprexpo/overview.hpp b/hyprexpo/overview.hpp index 144add9..0f0a1b5 100644 --- a/hyprexpo/overview.hpp +++ b/hyprexpo/overview.hpp @@ -5,9 +5,11 @@ #include "globals.hpp" #include #include +#include #include #include #include +#include // saves on resources, but is a bit broken rn with blur. // hyprland's fault, but cba to fix. @@ -17,6 +19,18 @@ class CMonitor; class COverview { public: + enum class LabelAnchor { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT, + TOP_CENTER, + BOTTOM_CENTER, + CENTER_LEFT, + CENTER_RIGHT, + CENTER + }; + COverview(PHLWORKSPACE startedOn_, bool swipe = false); ~COverview(); @@ -46,10 +60,18 @@ class COverview { void redrawAll(bool forcelowres = false); void onWorkspaceChange(); void fullRender(); + void renderText(SP out, const std::string& text, const CHyprColor& color, const Vector2D& bufferSize, const float scale, const int fontSize); + double calculateTextWidth(const std::string& text, const float scale, const int fontSize); + LabelAnchor parseLabelAnchor(const std::string& anchorStr); + Vector2D calculateTextPosition(const CBox& texbox, const Vector2D& textBufferSize, const double padding, LabelAnchor anchor); int SIDE_LENGTH = 3; int GAP_WIDTH = 5; CHyprColor BG_COLOR = CHyprColor{0.1, 0.1, 0.1, 1.0}; + bool show_label = false; + int fontSize = 24; + + LabelAnchor labelAnchor = LabelAnchor::TOP_LEFT; bool damageDirty = false; @@ -58,6 +80,7 @@ class COverview { int64_t workspaceID = -1; PHLWORKSPACE pWorkspace; CBox box; + SP textTex; }; Vector2D lastMousePosLocal = Vector2D{};