From 6a11bc35453a5c3e99b6b1060cae590e1121002c Mon Sep 17 00:00:00 2001 From: xavi-b Date: Fri, 19 Dec 2025 00:18:01 +0100 Subject: [PATCH] general: add screen cast border indicators --- src/config/ConfigDescriptions.hpp | 12 +++++ src/config/ConfigManager.cpp | 2 + src/protocols/Screencopy.cpp | 5 +++ src/protocols/Screencopy.hpp | 9 ++-- src/protocols/ToplevelExport.cpp | 6 ++- src/protocols/ToplevelExport.hpp | 9 ++-- src/render/Renderer.cpp | 44 +++++++++++++++++++ .../decorations/CHyprBorderDecoration.cpp | 25 +++++++++++ 8 files changed, 103 insertions(+), 9 deletions(-) diff --git a/src/config/ConfigDescriptions.hpp b/src/config/ConfigDescriptions.hpp index 38bb0a20c..9fd9d784f 100644 --- a/src/config/ConfigDescriptions.hpp +++ b/src/config/ConfigDescriptions.hpp @@ -15,6 +15,18 @@ inline static const std::vector CONFIG_OPTIONS = { .type = CONFIG_OPTION_INT, .data = SConfigOptionDescription::SRangeData{1, 0, 20}, }, + SConfigOptionDescription{ + .value = "general:screencast_border.enabled", + .description = "enable red border indicator when screen sharing", + .type = CONFIG_OPTION_BOOL, + .data = SConfigOptionDescription::SBoolData{true}, + }, + SConfigOptionDescription{ + .value = "general:screencast_border.color", + .description = "border color when screen sharing", + .type = CONFIG_OPTION_GRADIENT, + .data = SConfigOptionDescription::SGradientData{"0xffff0000"}, + }, SConfigOptionDescription{ .value = "general:gaps_in", .description = "gaps between windows\n\nsupports css style gaps (top, right, bottom, left -> 5 10 15 20)", diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 1af5fb150..f5d851d55 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -443,6 +443,8 @@ CConfigManager::CConfigManager() { m_config = makeUnique(m_configPaths.begin()->c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = true}); registerConfigVar("general:border_size", Hyprlang::INT{1}); + registerConfigVar("general:screencast_border.enabled", Hyprlang::INT{1}); + registerConfigVar("general:screencast_border.color", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0xffff0000"}); registerConfigVar("general:gaps_in", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "5"}); registerConfigVar("general:gaps_out", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "20"}); registerConfigVar("general:float_gaps", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "0"}); diff --git a/src/protocols/Screencopy.cpp b/src/protocols/Screencopy.cpp index 5507b5b39..5114d956e 100644 --- a/src/protocols/Screencopy.cpp +++ b/src/protocols/Screencopy.cpp @@ -514,6 +514,11 @@ void CScreencopyProtocol::destroyResource(CScreencopyClient* client) { } void CScreencopyProtocol::destroyResource(CScreencopyFrame* frame) { + // Damage the monitor before destroying the frame so the border updates + if (frame && frame->m_monitor) { + if (auto monitor = frame->m_monitor.lock()) + g_pHyprRenderer->damageMonitor(monitor); + } std::erase_if(m_frames, [&](const auto& other) { return other.get() == frame; }); std::erase_if(m_framesAwaitingWrite, [&](const auto& other) { return !other || other.get() == frame; }); } diff --git a/src/protocols/Screencopy.hpp b/src/protocols/Screencopy.hpp index 54b0d28c6..fcdb79a71 100644 --- a/src/protocols/Screencopy.hpp +++ b/src/protocols/Screencopy.hpp @@ -36,12 +36,13 @@ class CScreencopyClient { CTimer m_lastFrame; int m_frameCounter = 0; + bool m_sentScreencast = false; + private: SP m_resource; int m_framesInLastHalfSecond = 0; CTimer m_lastMeasure; - bool m_sentScreencast = false; SP m_tickCallback; void onTick(); @@ -59,11 +60,10 @@ class CScreencopyFrame { WP m_self; WP m_client; + PHLMONITORREF m_monitor; private: SP m_resource; - - PHLMONITORREF m_monitor; bool m_overlayCursor = false; bool m_withDamage = false; @@ -97,8 +97,9 @@ class CScreencopyProtocol : public IWaylandProtocol { void onOutputCommit(PHLMONITOR pMonitor); - private: std::vector> m_frames; + + private: std::vector> m_framesAwaitingWrite; std::vector> m_clients; diff --git a/src/protocols/ToplevelExport.cpp b/src/protocols/ToplevelExport.cpp index 9c9c1e1ed..6668170c8 100644 --- a/src/protocols/ToplevelExport.cpp +++ b/src/protocols/ToplevelExport.cpp @@ -74,7 +74,7 @@ bool CToplevelExportClient::good() { return m_resource->resource(); } -CToplevelExportFrame::CToplevelExportFrame(SP resource_, int32_t overlayCursor_, PHLWINDOW pWindow_) : m_resource(resource_), m_window(pWindow_) { +CToplevelExportFrame::CToplevelExportFrame(SP resource_, int32_t overlayCursor_, PHLWINDOW pWindow_) : m_window(pWindow_), m_resource(resource_) { if UNLIKELY (!good()) return; @@ -411,6 +411,10 @@ void CToplevelExportProtocol::destroyResource(CToplevelExportClient* client) { } void CToplevelExportProtocol::destroyResource(CToplevelExportFrame* frame) { + // Damage the window before destroying the frame so the border updates + if (frame && frame->m_window) { + g_pHyprRenderer->damageWindow(frame->m_window); + } std::erase_if(m_frames, [&](const auto& other) { return other.get() == frame; }); std::erase_if(m_framesAwaitingWrite, [&](const auto& other) { return !other || other.get() == frame; }); } diff --git a/src/protocols/ToplevelExport.hpp b/src/protocols/ToplevelExport.hpp index 44704d844..3a29487d7 100644 --- a/src/protocols/ToplevelExport.hpp +++ b/src/protocols/ToplevelExport.hpp @@ -22,12 +22,13 @@ class CToplevelExportClient { CTimer m_lastFrame; int m_frameCounter = 0; + bool m_sentScreencast = false; + private: SP m_resource; int m_framesInLastHalfSecond = 0; CTimer m_lastMeasure; - bool m_sentScreencast = false; SP m_tickCallback; void onTick(); @@ -45,11 +46,10 @@ class CToplevelExportFrame { WP m_self; WP m_client; + PHLWINDOW m_window; private: SP m_resource; - - PHLWINDOW m_window; bool m_cursorOverlayRequested = false; bool m_ignoreDamage = false; @@ -79,9 +79,10 @@ class CToplevelExportProtocol : IWaylandProtocol { void onOutputCommit(PHLMONITOR pMonitor); + std::vector> m_frames; + private: std::vector> m_clients; - std::vector> m_frames; std::vector> m_framesAwaitingWrite; void onWindowUnmap(PHLWINDOW pWindow); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 40b6cc89c..9b3d1af75 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -41,6 +41,10 @@ #include "../protocols/types/ContentType.hpp" #include "../helpers/MiscFunctions.hpp" #include "render/OpenGL.hpp" +#include "../protocols/Screencopy.hpp" +#include "../config/ConfigDataValues.hpp" +#include "../helpers/Color.hpp" +#include "pass/BorderPassElement.hpp" #include using namespace Hyprutils::Utils; @@ -1457,6 +1461,46 @@ void CHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { m_renderPass.add(makeUnique(data)); } + // Render border around monitor if it's being shared + static auto PENABLED = CConfigValue("general:screencast_border.enabled"); + if (*PENABLED) { + // Check if monitor is being shared through screencopy + bool isMonitorShared = false; + if (PROTO::screencopy) { + for (const auto& frame : PROTO::screencopy->m_frames) { + if (frame && frame->m_monitor && frame->m_monitor.lock() == pMonitor) { + isMonitorShared = frame->m_client->m_sentScreencast; + break; + } + } + } + + if (isMonitorShared) { + static auto PBORDERSIZE = CConfigValue("general:border_size"); + static auto PCOLOR = CConfigValue("general:screencast_border.color"); + const int borderSize = *PBORDERSIZE > 0 ? *PBORDERSIZE : 1; + + const int scaledBorderSize = std::round(borderSize * pMonitor->m_scale); + CBox monitorBox = {scaledBorderSize, scaledBorderSize, std::max(1, sc(pMonitor->m_transformedSize.x) - (2 * scaledBorderSize)), + std::max(1, sc(pMonitor->m_transformedSize.y) - (2 * scaledBorderSize))}; + + const auto* const PGRAD = sc((PCOLOR.ptr())->getData()); + CGradientValueData borderGrad = PGRAD ? *PGRAD : CGradientValueData(Colors::RED); // fallback to red if config not loaded + + CBorderPassElement::SBorderData borderData; + borderData.box = monitorBox; + borderData.grad1 = borderGrad; + borderData.round = 0; + borderData.outerRound = -1; + borderData.roundingPower = 2.0f; + borderData.a = 1.0f; + borderData.borderSize = borderSize; + borderData.hasGrad2 = false; + + m_renderPass.add(makeUnique(borderData)); + } + } + EMIT_HOOK_EVENT("render", RENDER_LAST_MOMENT); endRender(); diff --git a/src/render/decorations/CHyprBorderDecoration.cpp b/src/render/decorations/CHyprBorderDecoration.cpp index a082f0738..42a9a0327 100644 --- a/src/render/decorations/CHyprBorderDecoration.cpp +++ b/src/render/decorations/CHyprBorderDecoration.cpp @@ -5,6 +5,10 @@ #include "../pass/BorderPassElement.hpp" #include "../Renderer.hpp" #include "../../managers/HookSystemManager.hpp" +#include "../../protocols/Screencopy.hpp" +#include "../../protocols/ToplevelExport.hpp" +#include "../../helpers/Color.hpp" +#include "../../config/ConfigDataValues.hpp" CHyprBorderDecoration::CHyprBorderDecoration(PHLWINDOW pWindow) : IHyprWindowDecoration(pWindow), m_window(pWindow) { ; @@ -60,6 +64,27 @@ void CHyprBorderDecoration::draw(PHLMONITOR pMonitor, float const& a) { auto grad = m_window->m_realBorderColor; const bool ANIMATED = m_window->m_borderFadeAnimationProgress->isBeingAnimated(); + static auto PENABLED = CConfigValue("general:screencast_border.enabled"); + static auto PCOLOR = CConfigValue("general:screencast_border.color"); + + if (*PENABLED) { + // Check if window is being shared through toplevel export + bool isWindowShared = false; + if (PROTO::toplevelExport) { + for (const auto& frame : PROTO::toplevelExport->m_frames) { + if (frame && frame->m_window && frame->m_window == m_window) { + isWindowShared = frame->m_client->m_sentScreencast; + break; + } + } + } + + if (isWindowShared) { + const auto* const PGRAD = sc((PCOLOR.ptr())->getData()); + grad = PGRAD ? *PGRAD : CGradientValueData(Colors::RED); // fallback to red if config not loaded + } + } + if (m_window->m_borderAngleAnimationProgress->enabled()) { grad.m_angle += m_window->m_borderAngleAnimationProgress->value() * M_PI * 2; grad.m_angle = normalizeAngleRad(grad.m_angle);