Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hyprexpo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions hyprexpo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,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});

Expand Down
201 changes: 194 additions & 7 deletions hyprexpo/overview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <hyprland/src/helpers/time/Time.hpp>
#undef private
#include "OverviewPassElement.hpp"
#include <pango/pangocairo.h>

static void damageMonitor(WP<Hyprutils::Animation::CBaseAnimatedVariable> thisptr) {
g_pOverview->damage();
Expand All @@ -32,15 +33,22 @@ COverview::COverview(PHLWORKSPACE startedOn_, bool swipe_) : startedOn(startedOn
const auto PMONITOR = g_pCompositor->m_lastMonitor.lock();
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;
Expand Down Expand Up @@ -144,6 +152,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<CTexture>();

CRegion fakeDamage{0, 0, INT16_MAX, INT16_MAX};
g_pHyprRenderer->beginRender(PMONITOR, fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &image.fb);
Expand Down Expand Up @@ -423,6 +432,140 @@ void COverview::render() {
g_pHyprRenderer->m_renderPass.add(makeUnique<COverviewPassElement>());
}

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<CTexture> 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;

Expand All @@ -440,11 +583,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});
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions hyprexpo/overview.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
#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/managers/HookSystemManager.hpp>
#include <vector>
#include <pango/pangocairo.h>

// saves on resources, but is a bit broken rn with blur.
// hyprland's fault, but cba to fix.
Expand All @@ -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();

Expand Down Expand Up @@ -46,10 +60,18 @@ class COverview {
void redrawAll(bool forcelowres = false);
void onWorkspaceChange();
void fullRender();
void renderText(SP<CTexture> 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;

Expand All @@ -58,6 +80,7 @@ class COverview {
int64_t workspaceID = -1;
PHLWORKSPACE pWorkspace;
CBox box;
SP<CTexture> textTex;
};

Vector2D lastMousePosLocal = Vector2D{};
Expand Down