diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90314ef --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: AfterColon +ColumnLimit: 180 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +IncludeBlocks: Preserve +IndentCaseLabels: true +IndentWidth: 4 +PointerAlignment: Left +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never + +AllowShortEnumsOnASingleLine: false + +BraceWrapping: + AfterEnum: false + +AlignConsecutiveDeclarations: AcrossEmptyLines + +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33419bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/ +build/ +protocols/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4b29316 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.19) + +set(VERSION 0.1.0) + +project(hyprlock + DESCRIPTION "A gpu-accelerated screen lock for Hyprland" + VERSION ${VERSION} +) + +set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Configuring hyprlock in Debug with CMake") + add_compile_definitions(HYPRLAND_DEBUG) +else() + add_compile_options(-O3) + message(STATUS "Configuring hyprlock in Release with CMake") +endif() + +include_directories( + . + "protocols/" +) + +# configure +set(CMAKE_CXX_STANDARD 23) +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value + -Wno-missing-field-initializers -Wno-narrowing) + +# dependencies +message(STATUS "Checking deps...") + +find_package(Threads REQUIRED) +find_package(PkgConfig REQUIRED) +find_package(OpenGL REQUIRED) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-client wayland-protocols wayland-egl hyprlang>=0.4.0 egl opengl xkbcommon cairo pango pam) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") +add_executable(hyprlock ${SRCFILES}) +target_link_libraries(hyprlock PRIVATE rt Threads::Threads PkgConfig::deps OpenGL::EGL OpenGL::GL) + +# protocols +find_program(WaylandScanner NAMES wayland-scanner) +message(STATUS "Found WaylandScanner at ${WaylandScanner}") +execute_process( + COMMAND pkg-config --variable=pkgdatadir wayland-protocols + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) +message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") + +function(protocol protoPath protoName external) + if (external) + execute_process( + COMMAND ${WaylandScanner} client-header ${protoPath} protocols/${protoName}-protocol.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + execute_process( + COMMAND ${WaylandScanner} private-code ${protoPath} protocols/${protoName}-protocol.c + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + target_sources(hyprlock PRIVATE protocols/${protoName}-protocol.c) + else() + execute_process( + COMMAND ${WaylandScanner} client-header ${WAYLAND_PROTOCOLS_DIR}/${protoPath} protocols/${protoName}-protocol.h + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + execute_process( + COMMAND ${WaylandScanner} private-code ${WAYLAND_PROTOCOLS_DIR}/${protoPath} protocols/${protoName}-protocol.c + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + target_sources(hyprlock PRIVATE protocols/${protoName}-protocol.c) + endif() +endfunction() + +make_directory(${CMAKE_SOURCE_DIR}/protocols) # we don't ship any custom ones so the dir won't be there +protocol("staging/ext-session-lock/ext-session-lock-v1.xml" "ext-session-lock-v1" false) +protocol("staging/cursor-shape/cursor-shape-v1.xml" "cursor-shape-v1" false) +protocol("unstable/tablet/tablet-unstable-v2.xml" "tablet-unstable-v2" false) +protocol("staging/fractional-scale/fractional-scale-v1.xml" "fractional-scale-v1" false) +protocol("stable/viewporter/viewporter.xml" "viewporter" false) + +# Installation +install(TARGETS hyprlock) diff --git a/README.md b/README.md index cc04a83..f4b72e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,63 @@ # hyprlock -Hyprland's screen locking utility +Hyprland's simple, yet multi-threaded and GPU-accelerated screen locking utility. + +## Features + - uses the secure ext-session-lock protocol + - full support for fractional-scale + - fully GPU accelerated + - multi-threaded resource acquisition for no hitches + +## Example config + +```ini +general { + disable_loading_bar = false +} + +background { + monitor = DP-2 + path = /home/me/someImage.png +} + +background { + monitor = WL-2 + path = /home/me/someImage2.png +} + +input-field { + monitor = + size = 200, 50 + outline_thickness = 3 + outer_color = rgb(151515) + inner_color = rgb(200, 200, 200) +} +``` + +## Docs + +soon:tm: + +## Building + +### Deps + - wayland-client + - wayland-protocols + - cairo + - gles3.2 + - pango + - hyprlang>=0.4.0 + - xkbcommon + - pam + +### Building + +Building: +```sh +cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -S . -B ./build +cmake --build ./build --config Release --target hyprlock -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` +``` + +Installation: +```sh +sudo cmake --install build +``` diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp new file mode 100644 index 0000000..7a309a8 --- /dev/null +++ b/src/config/ConfigManager.cpp @@ -0,0 +1,84 @@ +#include "ConfigManager.hpp" +#include + +static std::string getConfigDir() { + static const char* xdgConfigHome = getenv("XDG_CONFIG_HOME"); + + if (xdgConfigHome && std::filesystem::path(xdgConfigHome).is_absolute()) + return xdgConfigHome; + + return getenv("HOME") + std::string("/.config"); +} + +static std::string getMainConfigPath() { + return getConfigDir() + "/hypr/hyprlock.conf"; +} + +CConfigManager::CConfigManager() : m_config(getMainConfigPath().c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = true}) { + ; +} + +void CConfigManager::init() { + m_config.addConfigValue("general:disable_loading_bar", Hyprlang::INT{0}); + + m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); + m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""}); + m_config.addSpecialConfigValue("background", "path", Hyprlang::STRING{""}); + + m_config.addSpecialCategory("input-field", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); + m_config.addSpecialConfigValue("input-field", "monitor", Hyprlang::STRING{""}); + m_config.addSpecialConfigValue("input-field", "size", Hyprlang::VEC2{400, 90}); + m_config.addSpecialConfigValue("input-field", "inner_color", Hyprlang::INT{0xFFDDDDDD}); + m_config.addSpecialConfigValue("input-field", "outer_color", Hyprlang::INT{0xFF111111}); + m_config.addSpecialConfigValue("input-field", "outline_thickness", Hyprlang::INT{4}); + + m_config.commence(); + + auto result = m_config.parse(); + + if (result.error) + Debug::log(ERR, "Config has errors:\n{}\nProceeding ignoring faulty entries", result.getError()); +} + +std::mutex configMtx; + +void* const* CConfigManager::getValuePtr(const std::string& name) { + std::lock_guard lg(configMtx); + return m_config.getConfigValuePtr(name.c_str())->getDataStaticPtr(); +} + +std::vector CConfigManager::getWidgetConfigs() { + std::vector result; + + // + auto keys = m_config.listKeysForSpecialCategory("background"); + for (auto& k : keys) { + // clang-format off + result.push_back(CConfigManager::SWidgetConfig{ + "background", + std::any_cast(m_config.getSpecialConfigValue("background", "monitor", k.c_str())), + { + {"path", m_config.getSpecialConfigValue("background", "path", k.c_str())}, + } + }); + // clang-format on + } + + keys = m_config.listKeysForSpecialCategory("input-field"); + for (auto& k : keys) { + // clang-format off + result.push_back(CConfigManager::SWidgetConfig{ + "input-field", + std::any_cast(m_config.getSpecialConfigValue("input-field", "monitor", k.c_str())), + { + {"size", m_config.getSpecialConfigValue("input-field", "size", k.c_str())}, + {"inner_color", m_config.getSpecialConfigValue("input-field", "inner_color", k.c_str())}, + {"outer_color", m_config.getSpecialConfigValue("input-field", "outer_color", k.c_str())}, + {"outline_thickness", m_config.getSpecialConfigValue("input-field", "outline_thickness", k.c_str())}, + } + }); + // clang-format on + } + + return result; +} \ No newline at end of file diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp new file mode 100644 index 0000000..c0ab72b --- /dev/null +++ b/src/config/ConfigManager.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "../helpers/Log.hpp" + +#include + +#include +#include +#include + +class CConfigManager { + public: + CConfigManager(); + void init(); + void* const* getValuePtr(const std::string& name); + + struct SWidgetConfig { + std::string type; + std::string monitor; + + std::unordered_map values; + }; + + std::vector getWidgetConfigs(); + + private: + Hyprlang::CConfig m_config; +}; + +inline std::unique_ptr g_pConfigManager; diff --git a/src/core/CursorShape.cpp b/src/core/CursorShape.cpp new file mode 100644 index 0000000..365348d --- /dev/null +++ b/src/core/CursorShape.cpp @@ -0,0 +1,16 @@ +#include "CursorShape.hpp" +#include "hyprlock.hpp" + +CCursorShape::CCursorShape(wp_cursor_shape_manager_v1* mgr) : mgr(mgr) { + if (!g_pHyprlock->m_pPointer) + return; + + dev = wp_cursor_shape_manager_v1_get_pointer(mgr, g_pHyprlock->m_pPointer); +} + +void CCursorShape::setShape(const uint32_t serial, const wp_cursor_shape_device_v1_shape shape) { + if (!dev) + return; + + wp_cursor_shape_device_v1_set_shape(dev, serial, shape); +} \ No newline at end of file diff --git a/src/core/CursorShape.hpp b/src/core/CursorShape.hpp new file mode 100644 index 0000000..65b0f39 --- /dev/null +++ b/src/core/CursorShape.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "cursor-shape-v1-protocol.h" + +class CCursorShape { + public: + CCursorShape(wp_cursor_shape_manager_v1* mgr); + + void setShape(const uint32_t serial, const wp_cursor_shape_device_v1_shape shape); + + private: + wp_cursor_shape_manager_v1* mgr = nullptr; + wp_cursor_shape_device_v1* dev = nullptr; +}; \ No newline at end of file diff --git a/src/core/Egl.cpp b/src/core/Egl.cpp new file mode 100644 index 0000000..71956a6 --- /dev/null +++ b/src/core/Egl.cpp @@ -0,0 +1,80 @@ +#include "Egl.hpp" +#include "../helpers/Log.hpp" + +PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; + +const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE, +}; + +const EGLint context_attribs[] = { + EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_NONE, +}; + +CEGL::CEGL(wl_display* display) { + const char* _EXTS = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (!_EXTS) { + if (eglGetError() == EGL_BAD_DISPLAY) + throw std::runtime_error("EGL_EXT_client_extensions not supported"); + else + throw std::runtime_error("Failed to query EGL client extensions"); + } + + std::string EXTS = _EXTS; + + if (!EXTS.contains("EGL_EXT_platform_base")) + throw std::runtime_error("EGL_EXT_platform_base not supported"); + + if (!EXTS.contains("EGL_EXT_platform_wayland")) + throw std::runtime_error("EGL_EXT_platform_wayland not supported"); + + eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (eglGetPlatformDisplayEXT == NULL) + throw std::runtime_error("Failed to get eglGetPlatformDisplayEXT"); + + eglCreatePlatformWindowSurfaceEXT = (PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC)eglGetProcAddress("eglCreatePlatformWindowSurfaceEXT"); + if (eglCreatePlatformWindowSurfaceEXT == NULL) + throw std::runtime_error("Failed to get eglCreatePlatformWindowSurfaceEXT"); + + eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT, display, NULL); + EGLint matched = 0; + if (eglDisplay == EGL_NO_DISPLAY) { + Debug::log(CRIT, "Failed to create EGL display"); + goto error; + } + + if (eglInitialize(eglDisplay, NULL, NULL) == EGL_FALSE) { + Debug::log(CRIT, "Failed to initialize EGL"); + goto error; + } + + if (!eglChooseConfig(eglDisplay, config_attribs, &eglConfig, 1, &matched)) { + Debug::log(CRIT, "eglChooseConfig failed"); + goto error; + } + if (matched == 0) { + Debug::log(CRIT, "Failed to match an EGL config"); + goto error; + } + + eglContext = eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, context_attribs); + if (eglContext == EGL_NO_CONTEXT) { + Debug::log(CRIT, "Failed to create EGL context"); + goto error; + } + + return; + +error: + eglMakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (eglDisplay) + eglTerminate(eglDisplay); + + eglReleaseThread(); +} + +void CEGL::makeCurrent(EGLSurface surf) { + eglMakeCurrent(eglDisplay, surf, surf, eglContext); +} \ No newline at end of file diff --git a/src/core/Egl.hpp b/src/core/Egl.hpp new file mode 100644 index 0000000..55f1db0 --- /dev/null +++ b/src/core/Egl.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include +#include + +class CEGL { + public: + CEGL(wl_display*); + + EGLDisplay eglDisplay; + EGLConfig eglConfig; + EGLContext eglContext; + + PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC eglCreatePlatformWindowSurfaceEXT; + + void makeCurrent(EGLSurface surf); +}; + +inline std::unique_ptr g_pEGL; \ No newline at end of file diff --git a/src/core/LockSurface.cpp b/src/core/LockSurface.cpp new file mode 100644 index 0000000..089ad63 --- /dev/null +++ b/src/core/LockSurface.cpp @@ -0,0 +1,150 @@ +#include "LockSurface.hpp" +#include "hyprlock.hpp" +#include "../helpers/Log.hpp" +#include "Egl.hpp" +#include "../renderer/Renderer.hpp" + +static void handleConfigure(void* data, ext_session_lock_surface_v1* surf, uint32_t serial, uint32_t width, uint32_t height) { + const auto PSURF = (CSessionLockSurface*)data; + PSURF->configure({width, height}, serial); +} + +static const ext_session_lock_surface_v1_listener lockListener = { + .configure = handleConfigure, +}; + +static void handlePreferredScale(void* data, wp_fractional_scale_v1* wp_fractional_scale_v1, uint32_t scale) { + const auto PSURF = (CSessionLockSurface*)data; + PSURF->fractionalScale = scale / 120.0; + Debug::log(LOG, "got fractional {}", PSURF->fractionalScale); +} + +static const wp_fractional_scale_v1_listener fsListener = { + .preferred_scale = handlePreferredScale, +}; + +CSessionLockSurface::~CSessionLockSurface() { + if (fractional) { + wp_viewport_destroy(viewport); + wp_fractional_scale_v1_destroy(fractional); + } + wl_egl_window_destroy(eglWindow); + ext_session_lock_surface_v1_destroy(lockSurface); + wl_surface_destroy(surface); +} + +CSessionLockSurface::CSessionLockSurface(COutput* output) : output(output) { + surface = wl_compositor_create_surface(g_pHyprlock->getCompositor()); + + if (!surface) { + Debug::log(CRIT, "Couldn't create wl_surface"); + exit(1); + } + + fractional = wp_fractional_scale_manager_v1_get_fractional_scale(g_pHyprlock->getFractionalMgr(), surface); + if (fractional) { + wp_fractional_scale_v1_add_listener(fractional, &fsListener, this); + viewport = wp_viewporter_get_viewport(g_pHyprlock->getViewporter(), surface); + wl_display_roundtrip(g_pHyprlock->getDisplay()); + } else { + Debug::log(LOG, "No fractional-scale support! Oops, won't be able to scale!"); + } + + configure(output->size, 0); + g_pRenderer->renderLock(*this); + + lockSurface = ext_session_lock_v1_get_lock_surface(g_pHyprlock->getSessionLock(), surface, output->output); + + if (!surface) { + Debug::log(CRIT, "Couldn't create ext_session_lock_surface_v1"); + exit(1); + } + + ext_session_lock_surface_v1_add_listener(lockSurface, &lockListener, this); + wl_display_roundtrip(g_pHyprlock->getDisplay()); + wl_display_flush(g_pHyprlock->getDisplay()); +} + +void CSessionLockSurface::configure(const Vector2D& size_, uint32_t serial_) { + Debug::log(LOG, "configure with serial {}", serial); + + serial = serial_; + size = (size_ * fractionalScale).floor(); + logicalSize = size_; + if (serial != 0) + ext_session_lock_surface_v1_ack_configure(lockSurface, serial); + + if (fractional) + wp_viewport_set_destination(viewport, logicalSize.x, logicalSize.y); + + wl_surface_set_buffer_scale(surface, 1); + wl_surface_damage_buffer(surface, 0, 0, 0xFFFF, 0xFFFF); + + if (!eglWindow) + eglWindow = wl_egl_window_create(surface, size.x, size.y); + else + wl_egl_window_resize(eglWindow, size.x, size.y, 0, 0); + + if (!eglWindow) { + Debug::log(CRIT, "Couldn't create eglWindow"); + exit(1); + } + + if (serial == 0) + eglSurface = g_pEGL->eglCreatePlatformWindowSurfaceEXT(g_pEGL->eglDisplay, g_pEGL->eglConfig, eglWindow, nullptr); + + if (!eglSurface) { + Debug::log(CRIT, "Couldn't create eglSurface"); + exit(1); + } + + readyForFrame = true; + + if (serial != 0) + render(); + + if (fractional) + wp_viewport_set_destination(viewport, logicalSize.x, logicalSize.y); + + wl_surface_set_buffer_scale(surface, 1); + wl_surface_damage_buffer(surface, 0, 0, 0xFFFF, 0xFFFF); + + wl_surface_commit(surface); + wl_display_roundtrip(g_pHyprlock->getDisplay()); + wl_display_flush(g_pHyprlock->getDisplay()); +} + +static void handleDone(void* data, wl_callback* wl_callback, uint32_t callback_data) { + const auto PSURF = (CSessionLockSurface*)data; + PSURF->onCallback(); +} + +static const wl_callback_listener callbackListener = { + .done = handleDone, +}; + +void CSessionLockSurface::render() { + Debug::log(LOG, "render lock"); + + const auto FEEDBACK = g_pRenderer->renderLock(*this); + frameCallback = wl_surface_frame(surface); + wl_callback_add_listener(frameCallback, &callbackListener, this); + eglSwapBuffers(g_pEGL->eglDisplay, eglSurface); + + if (fractional) + wp_viewport_set_destination(viewport, logicalSize.x, logicalSize.y); + + wl_surface_damage_buffer(surface, 0, 0, 0xFFFF, 0xFFFF); + wl_surface_set_buffer_scale(surface, 1); + wl_surface_commit(surface); + + needsFrame = FEEDBACK.needsFrame; +} + +void CSessionLockSurface::onCallback() { + readyForFrame = true; + frameCallback = nullptr; + + if (needsFrame && !g_pHyprlock->m_bTerminate && g_pEGL) + render(); +} \ No newline at end of file diff --git a/src/core/LockSurface.hpp b/src/core/LockSurface.hpp new file mode 100644 index 0000000..2f93521 --- /dev/null +++ b/src/core/LockSurface.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include "ext-session-lock-v1-protocol.h" +#include "viewporter-protocol.h" +#include "fractional-scale-v1-protocol.h" +#include +#include "../helpers/Vector2D.hpp" +#include + +class COutput; +class CRenderer; + +class CSessionLockSurface { + public: + CSessionLockSurface(COutput* output); + ~CSessionLockSurface(); + + void configure(const Vector2D& size, uint32_t serial); + + bool readyForFrame = false; + + float fractionalScale = 1.0; + + void render(); + void onCallback(); + + private: + COutput* output = nullptr; + wl_surface* surface = nullptr; + ext_session_lock_surface_v1* lockSurface = nullptr; + uint32_t serial = 0; + wl_egl_window* eglWindow = nullptr; + Vector2D size; + Vector2D logicalSize; + EGLSurface eglSurface = nullptr; + wp_fractional_scale_v1* fractional = nullptr; + wp_viewport* viewport = nullptr; + + bool needsFrame = false; + + // wayland callbacks + wl_callback* frameCallback = nullptr; + + friend class CRenderer; +}; \ No newline at end of file diff --git a/src/core/Output.cpp b/src/core/Output.cpp new file mode 100644 index 0000000..cebdab3 --- /dev/null +++ b/src/core/Output.cpp @@ -0,0 +1,49 @@ +#include "Output.hpp" +#include "../helpers/Log.hpp" + +static void handleGeometry(void* data, wl_output* output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, + const char* model, int32_t transform) { + const auto POUTPUT = (COutput*)data; + POUTPUT->transform = (wl_output_transform)transform; + + Debug::log(LOG, "output {} make {} model {}", POUTPUT->name, make ? make : "", model ? model : ""); +} + +static void handleMode(void* data, wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + const auto POUTPUT = (COutput*)data; + POUTPUT->size = {width, height}; +} + +static void handleDone(void* data, wl_output* output) { + ; +} + +static void handleScale(void* data, wl_output* output, int32_t factor) { + const auto POUTPUT = (COutput*)data; + POUTPUT->scale = factor; +} + +static void handleName(void* data, wl_output* output, const char* name) { + const auto POUTPUT = (COutput*)data; + POUTPUT->stringName = std::string{name} + POUTPUT->stringName; + POUTPUT->stringPort = std::string{name}; + Debug::log(LOG, "output {} name {}", POUTPUT->name, name); +} + +static void handleDescription(void* data, wl_output* output, const char* description) { + const auto POUTPUT = (COutput*)data; + Debug::log(LOG, "output {} description {}", POUTPUT->name, description ? description : ""); +} + +static const wl_output_listener outputListener = { + .geometry = handleGeometry, + .mode = handleMode, + .done = handleDone, + .scale = handleScale, + .name = handleName, + .description = handleDescription, +}; + +COutput::COutput(wl_output* output, uint32_t name) : name(name), output(output) { + wl_output_add_listener(output, &outputListener, this); +} \ No newline at end of file diff --git a/src/core/Output.hpp b/src/core/Output.hpp new file mode 100644 index 0000000..5525874 --- /dev/null +++ b/src/core/Output.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "../helpers/Vector2D.hpp" +#include "LockSurface.hpp" +#include + +class COutput { + public: + COutput(wl_output* output, uint32_t name); + + uint32_t name = 0; + bool focused = false; + wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; + Vector2D size; + int scale = 1; + std::string stringName = ""; + std::string stringPort = ""; + + std::unique_ptr sessionLockSurface; + + wl_output* output = nullptr; + + private: +}; \ No newline at end of file diff --git a/src/core/Password.cpp b/src/core/Password.cpp new file mode 100644 index 0000000..ee749be --- /dev/null +++ b/src/core/Password.cpp @@ -0,0 +1,36 @@ +#include "Password.hpp" + +#include +#include +#include + +struct pam_response* reply; + +// +int conv(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { + *resp = reply; + return PAM_SUCCESS; +} + +CPassword::SVerificationResult CPassword::verify(const std::string& pass) { + const pam_conv localConv = {conv, NULL}; + pam_handle_t* handle = NULL; + + int ret = pam_start("su", getlogin(), &localConv, &handle); + + if (ret != PAM_SUCCESS) + return {false, "pam_start failed"}; + + reply = (struct pam_response*)malloc(sizeof(struct pam_response)); + + reply->resp = strdup(pass.c_str()); + reply->resp_retcode = 0; + ret = pam_authenticate(handle, 0); + + if (ret != PAM_SUCCESS) + return {false, ret == PAM_AUTH_ERR ? "Authentication failed" : "pam_authenticate failed"}; + + ret = pam_end(handle, ret); + + return {true, "Successfully authenticated"}; +} \ No newline at end of file diff --git a/src/core/Password.hpp b/src/core/Password.hpp new file mode 100644 index 0000000..951c180 --- /dev/null +++ b/src/core/Password.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +class CPassword { + public: + struct SVerificationResult { + bool success = false; + std::string failReason = ""; + }; + + SVerificationResult verify(const std::string& pass); +}; + +inline std::unique_ptr g_pPassword = std::make_unique(); \ No newline at end of file diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp new file mode 100644 index 0000000..8770518 --- /dev/null +++ b/src/core/hyprlock.cpp @@ -0,0 +1,375 @@ +#include "hyprlock.hpp" +#include "../helpers/Log.hpp" +#include "../config/ConfigManager.hpp" +#include "../renderer/Renderer.hpp" +#include "Password.hpp" +#include "Egl.hpp" +#include +#include + +CHyprlock::CHyprlock() { + m_sWaylandState.display = wl_display_connect("wayland-2"); + if (!m_sWaylandState.display) { + Debug::log(CRIT, "Couldn't connect to a wayland compositor"); + exit(1); + } + + g_pEGL = std::make_unique(m_sWaylandState.display); + + m_pXKBContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (!m_pXKBContext) + Debug::log(ERR, "Failed to create xkb context"); + + g_pRenderer = std::make_unique(); +} + +// wl_seat + +static void handleCapabilities(void* data, wl_seat* wl_seat, uint32_t capabilities); +static void handleName(void* data, struct wl_seat* wl_seat, const char* name); + +inline const wl_seat_listener seatListener = { + .capabilities = handleCapabilities, + .name = handleName, +}; + +// end wl_seat + +// wl_registry + +static void handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + g_pHyprlock->onGlobal(data, registry, name, interface, version); +} + +static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name) { + g_pHyprlock->onGlobalRemoved(data, registry, name); +} + +inline const wl_registry_listener registryListener = { + .global = handleGlobal, + .global_remove = handleGlobalRemove, +}; + +void CHyprlock::onGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + const std::string IFACE = interface; + Debug::log(LOG, " | got iface: {} v{}", IFACE, version); + + if (IFACE == ext_session_lock_manager_v1_interface.name) { + m_sWaylandState.sessionLock = (ext_session_lock_manager_v1*)wl_registry_bind(registry, name, &ext_session_lock_manager_v1_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wl_seat_interface.name) { + if (m_sWaylandState.seat) { + Debug::log(WARN, "Hyprlock does not support multi-seat configurations. Only binding to the first seat."); + return; + } + + m_sWaylandState.seat = (wl_seat*)wl_registry_bind(registry, name, &wl_seat_interface, version); + wl_seat_add_listener(m_sWaylandState.seat, &seatListener, nullptr); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wl_output_interface.name) { + m_vOutputs.emplace_back(std::make_unique((wl_output*)wl_registry_bind(registry, name, &wl_output_interface, version), name)); + + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wp_cursor_shape_manager_v1_interface.name) { + m_pCursorShape = std::make_unique((wp_cursor_shape_manager_v1*)wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, version)); + + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wl_compositor_interface.name) { + m_sWaylandState.compositor = (wl_compositor*)wl_registry_bind(registry, name, &wl_compositor_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wp_fractional_scale_manager_v1_interface.name) { + m_sWaylandState.fractional = (wp_fractional_scale_manager_v1*)wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } else if (IFACE == wp_viewporter_interface.name) { + m_sWaylandState.viewporter = (wp_viewporter*)wl_registry_bind(registry, name, &wp_viewporter_interface, version); + Debug::log(LOG, " > Bound to {} v{}", IFACE, version); + } +} + +void CHyprlock::onGlobalRemoved(void* data, struct wl_registry* registry, uint32_t name) { + Debug::log(LOG, " | removed iface {}", name); + std::erase_if(m_vOutputs, [name](const auto& other) { return other->name == name; }); +} + +// end wl_registry + +void CHyprlock::run() { + m_sWaylandState.registry = wl_display_get_registry(m_sWaylandState.display); + + wl_registry_add_listener(m_sWaylandState.registry, ®istryListener, nullptr); + + wl_display_roundtrip(m_sWaylandState.display); + + if (!m_sWaylandState.sessionLock) { + Debug::log(CRIT, "Couldn't bind to ext-session-lock-v1, does your compositor support it?"); + exit(1); + } + + // gather info about monitors + wl_display_roundtrip(m_sWaylandState.display); + + lockSession(); + + while (wl_display_dispatch(m_sWaylandState.display) != -1) { + if (m_bTerminate) + break; + } + + Debug::log(LOG, "Reached the end, exiting"); +} + +// wl_seat + +static void handlePointerEnter(void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { + if (!g_pHyprlock->m_pCursorShape) + return; + + g_pHyprlock->m_pCursorShape->setShape(serial, wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER); +} + +static void handlePointerLeave(void* data, struct wl_pointer* wl_pointer, uint32_t serial, struct wl_surface* surface) { + ; +} + +static void handlePointerAxis(void* data, wl_pointer* wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { + // ignored +} + +static void handlePointerMotion(void* data, struct wl_pointer* wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { + ; +} + +static void handlePointerButton(void* data, struct wl_pointer* wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t button_state) { + ; + // TODO: remove xD + g_pHyprlock->unlockSession(); +} + +static void handleFrame(void* data, struct wl_pointer* wl_pointer) { + ; +} + +static void handleAxisSource(void* data, struct wl_pointer* wl_pointer, uint32_t axis_source) { + ; +} + +static void handleAxisStop(void* data, struct wl_pointer* wl_pointer, uint32_t time, uint32_t axis) { + ; +} + +static void handleAxisDiscrete(void* data, struct wl_pointer* wl_pointer, uint32_t axis, int32_t discrete) { + ; +} + +static void handleAxisValue120(void* data, struct wl_pointer* wl_pointer, uint32_t axis, int32_t value120) { + ; +} + +static void handleAxisRelativeDirection(void* data, struct wl_pointer* wl_pointer, uint32_t axis, uint32_t direction) { + ; +} + +inline const wl_pointer_listener pointerListener = { + .enter = handlePointerEnter, + .leave = handlePointerLeave, + .motion = handlePointerMotion, + .button = handlePointerButton, + .axis = handlePointerAxis, + .frame = handleFrame, + .axis_source = handleAxisSource, + .axis_stop = handleAxisStop, + .axis_discrete = handleAxisDiscrete, + .axis_value120 = handleAxisValue120, + .axis_relative_direction = handleAxisRelativeDirection, +}; + +static void handleKeyboardKeymap(void* data, wl_keyboard* wl_keyboard, uint format, int fd, uint size) { + if (!g_pHyprlock->m_pXKBContext) + return; + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + Debug::log(ERR, "Could not recognise keymap format"); + return; + } + + const char* buf = (const char*)mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + if (buf == MAP_FAILED) { + Debug::log(ERR, "Failed to mmap xkb keymap: %d", errno); + return; + } + + g_pHyprlock->m_pXKBKeymap = xkb_keymap_new_from_buffer(g_pHyprlock->m_pXKBContext, buf, size - 1, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + + munmap((void*)buf, size); + close(fd); + + if (!g_pHyprlock->m_pXKBKeymap) { + Debug::log(ERR, "Failed to compile xkb keymap"); + return; + } + + g_pHyprlock->m_pXKBState = xkb_state_new(g_pHyprlock->m_pXKBKeymap); + if (!g_pHyprlock->m_pXKBState) { + Debug::log(ERR, "Failed to create xkb state"); + return; + } +} + +static void handleKeyboardKey(void* data, struct wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { + if (state != WL_KEYBOARD_KEY_STATE_PRESSED) + return; + + g_pHyprlock->onKey(key); +} + +static void handleKeyboardEnter(void* data, wl_keyboard* wl_keyboard, uint serial, wl_surface* surface, wl_array* keys) { + ; +} + +static void handleKeyboardLeave(void* data, wl_keyboard* wl_keyboard, uint serial, wl_surface* surface) { + ; +} + +static void handleKeyboardModifiers(void* data, wl_keyboard* wl_keyboard, uint serial, uint mods_depressed, uint mods_latched, uint mods_locked, uint group) { + if (!g_pHyprlock->m_pXKBState) + return; + + xkb_state_update_mask(g_pHyprlock->m_pXKBState, mods_depressed, mods_latched, mods_locked, 0, 0, group); +} + +static void handleRepeatInfo(void* data, struct wl_keyboard* wl_keyboard, int32_t rate, int32_t delay) { + ; +} + +inline const wl_keyboard_listener keyboardListener = { + .keymap = handleKeyboardKeymap, + .enter = handleKeyboardEnter, + .leave = handleKeyboardLeave, + .key = handleKeyboardKey, + .modifiers = handleKeyboardModifiers, + .repeat_info = handleRepeatInfo, +}; + +static void handleCapabilities(void* data, wl_seat* wl_seat, uint32_t capabilities) { + if (capabilities & WL_SEAT_CAPABILITY_POINTER) { + g_pHyprlock->m_pPointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(g_pHyprlock->m_pPointer, &pointerListener, wl_seat); + } + + if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { + g_pHyprlock->m_pKeeb = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(g_pHyprlock->m_pKeeb, &keyboardListener, wl_seat); + } +} + +static void handleName(void* data, struct wl_seat* wl_seat, const char* name) { + ; +} + +// end wl_seat + +// session_lock + +static void handleLocked(void* data, ext_session_lock_v1* ext_session_lock_v1) { + g_pHyprlock->onLockLocked(); +} + +static void handleFinished(void* data, ext_session_lock_v1* ext_session_lock_v1) { + g_pHyprlock->onLockFinished(); +} + +static const ext_session_lock_v1_listener sessionLockListener = { + .locked = handleLocked, + .finished = handleFinished, +}; + +// end session_lock + +void CHyprlock::onKey(uint32_t key) { + const auto SYM = xkb_state_key_get_one_sym(m_pXKBState, key + 8); + + if (SYM == XKB_KEY_BackSpace) { + if (m_sPasswordState.passBuffer.length() > 0) + m_sPasswordState.passBuffer = m_sPasswordState.passBuffer.substr(0, m_sPasswordState.passBuffer.length() - 1); + } else if (SYM == XKB_KEY_Return) { + Debug::log(LOG, "Authenticating"); + + const auto RESULT = g_pPassword->verify(m_sPasswordState.passBuffer); + + Debug::log(LOG, "Password auth result: {}", RESULT.failReason); + + if (RESULT.success) + unlockSession(); + } else { + char buf[16] = {0}; + int len = xkb_keysym_to_utf8(SYM, buf, 16); + if (len > 1) + m_sPasswordState.passBuffer += std::string{buf, len - 1}; + } + + for (auto& o : m_vOutputs) { + o->sessionLockSurface->render(); + } +} + +void CHyprlock::lockSession() { + Debug::log(LOG, "Locking session"); + m_sLockState.lock = ext_session_lock_manager_v1_lock(m_sWaylandState.sessionLock); + ext_session_lock_v1_add_listener(m_sLockState.lock, &sessionLockListener, nullptr); +} + +void CHyprlock::unlockSession() { + Debug::log(LOG, "Unlocking session"); + ext_session_lock_v1_unlock_and_destroy(m_sLockState.lock); + m_sLockState.lock = nullptr; + + m_vOutputs.clear(); + g_pEGL.reset(); + Debug::log(LOG, "Unlocked, exiting!"); + + m_bTerminate = true; + + wl_display_roundtrip(m_sWaylandState.display); +} + +void CHyprlock::onLockLocked() { + Debug::log(LOG, "onLockLocked called"); + + for (auto& o : m_vOutputs) { + o->sessionLockSurface = std::make_unique(o.get()); + } +} + +void CHyprlock::onLockFinished() { + Debug::log(LOG, "onLockFinished called. Seems we got yeeten. Is another lockscreen running?"); + ext_session_lock_v1_unlock_and_destroy(m_sLockState.lock); + m_sLockState.lock = nullptr; +} + +ext_session_lock_manager_v1* CHyprlock::getSessionLockMgr() { + return m_sWaylandState.sessionLock; +} + +ext_session_lock_v1* CHyprlock::getSessionLock() { + return m_sLockState.lock; +} + +wl_compositor* CHyprlock::getCompositor() { + return m_sWaylandState.compositor; +} + +wl_display* CHyprlock::getDisplay() { + return m_sWaylandState.display; +} + +wp_fractional_scale_manager_v1* CHyprlock::getFractionalMgr() { + return m_sWaylandState.fractional; +} + +wp_viewporter* CHyprlock::getViewporter() { + return m_sWaylandState.viewporter; +} + +size_t CHyprlock::getPasswordBufferLen() { + return m_sPasswordState.passBuffer.length(); +} diff --git a/src/core/hyprlock.hpp b/src/core/hyprlock.hpp new file mode 100644 index 0000000..7e82e18 --- /dev/null +++ b/src/core/hyprlock.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include "ext-session-lock-v1-protocol.h" +#include "fractional-scale-v1-protocol.h" +#include "viewporter-protocol.h" +#include "Output.hpp" +#include "CursorShape.hpp" + +#include +#include + +#include + +class CHyprlock { + public: + CHyprlock(); + + void run(); + + void onGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version); + void onGlobalRemoved(void* data, struct wl_registry* registry, uint32_t name); + + void onLockLocked(); + void onLockFinished(); + + void lockSession(); + void unlockSession(); + + void onKey(uint32_t key); + + size_t getPasswordBufferLen(); + + ext_session_lock_manager_v1* getSessionLockMgr(); + ext_session_lock_v1* getSessionLock(); + wl_compositor* getCompositor(); + wl_display* getDisplay(); + wp_fractional_scale_manager_v1* getFractionalMgr(); + wp_viewporter* getViewporter(); + + wl_pointer* m_pPointer = nullptr; + wl_keyboard* m_pKeeb = nullptr; + + std::unique_ptr m_pCursorShape; + + xkb_context* m_pXKBContext = nullptr; + xkb_keymap* m_pXKBKeymap = nullptr; + xkb_state* m_pXKBState = nullptr; + + bool m_bTerminate = false; + + private: + struct { + wl_display* display = nullptr; + wl_registry* registry = nullptr; + wl_seat* seat = nullptr; + ext_session_lock_manager_v1* sessionLock = nullptr; + wl_compositor* compositor = nullptr; + wp_fractional_scale_manager_v1* fractional = nullptr; + wp_viewporter* viewporter = nullptr; + } m_sWaylandState; + + struct { + ext_session_lock_v1* lock = nullptr; + } m_sLockState; + + struct { + std::string passBuffer = ""; + } m_sPasswordState; + + std::vector> m_vOutputs; +}; + +inline std::unique_ptr g_pHyprlock; \ No newline at end of file diff --git a/src/helpers/Box.cpp b/src/helpers/Box.cpp new file mode 100644 index 0000000..9b88218 --- /dev/null +++ b/src/helpers/Box.cpp @@ -0,0 +1,101 @@ +#include "Box.hpp" + +#include +#include + +#define VECINRECT(vec, x1, y1, x2, y2) ((vec).x >= (x1) && (vec).x <= (x2) && (vec).y >= (y1) && (vec).y <= (y2)) + +CBox& CBox::scale(double scale) { + x *= scale; + y *= scale; + w *= scale; + h *= scale; + + return *this; +} + +CBox& CBox::scale(const Vector2D& scale) { + x *= scale.x; + y *= scale.y; + w *= scale.x; + h *= scale.y; + + return *this; +} + +CBox& CBox::translate(const Vector2D& vec) { + x += vec.x; + y += vec.y; + + return *this; +} + +Vector2D CBox::middle() const { + return Vector2D{x + w / 2.0, y + h / 2.0}; +} + +bool CBox::containsPoint(const Vector2D& vec) const { + return VECINRECT(vec, x, y, x + w, y + h); +} + +bool CBox::empty() const { + return w == 0 || h == 0; +} + +CBox& CBox::round() { + float newW = x + w - std::round(x); + float newH = y + h - std::round(y); + x = std::round(x); + y = std::round(y); + w = std::round(newW); + h = std::round(newH); + + return *this; +} + +CBox& CBox::scaleFromCenter(double scale) { + double oldW = w, oldH = h; + + w *= scale; + h *= scale; + + x -= (w - oldW) / 2.0; + y -= (h - oldH) / 2.0; + + return *this; +} + +CBox& CBox::expand(const double& value) { + x -= value; + y -= value; + w += value * 2.0; + h += value * 2.0; + + return *this; +} + +CBox& CBox::noNegativeSize() { + std::clamp(w, 0.0, std::numeric_limits::infinity()); + std::clamp(h, 0.0, std::numeric_limits::infinity()); + + return *this; +} + +CBox CBox::roundInternal() { + float newW = x + w - std::floor(x); + float newH = y + h - std::floor(y); + + return CBox{std::floor(x), std::floor(y), std::floor(newW), std::floor(newH)}; +} + +CBox CBox::copy() const { + return CBox{*this}; +} + +Vector2D CBox::pos() const { + return {x, y}; +} + +Vector2D CBox::size() const { + return {w, h}; +} \ No newline at end of file diff --git a/src/helpers/Box.hpp b/src/helpers/Box.hpp new file mode 100644 index 0000000..c5f30d3 --- /dev/null +++ b/src/helpers/Box.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "Vector2D.hpp" + +class CBox { + public: + CBox(double x_, double y_, double w_, double h_) { + x = x_; + y = y_; + w = w_; + h = h_; + } + + CBox() { + w = 0; + h = 0; + } + + CBox(const double d) { + x = d; + y = d; + w = d; + h = d; + } + + CBox(const Vector2D& pos, const Vector2D& size) { + x = pos.x; + y = pos.y; + w = size.x; + h = size.y; + } + + CBox& scale(double scale); + CBox& scaleFromCenter(double scale); + CBox& scale(const Vector2D& scale); + CBox& translate(const Vector2D& vec); + CBox& round(); + CBox& expand(const double& value); + CBox& noNegativeSize(); + + CBox copy() const; + + Vector2D middle() const; + Vector2D pos() const; + Vector2D size() const; + + bool containsPoint(const Vector2D& vec) const; + bool empty() const; + + double x = 0, y = 0; + union { + double w; + double width; + }; + union { + double h; + double height; + }; + + double rot = 0; /* rad, ccw */ + + // + bool operator==(const CBox& rhs) const { + return x == rhs.x && y == rhs.y && w == rhs.w && h == rhs.h; + } + + private: + CBox roundInternal(); +}; diff --git a/src/helpers/Color.cpp b/src/helpers/Color.cpp new file mode 100644 index 0000000..77e0d53 --- /dev/null +++ b/src/helpers/Color.cpp @@ -0,0 +1,26 @@ +#include "Color.hpp" + +#define ALPHA(c) ((double)(((c) >> 24) & 0xff) / 255.0) +#define RED(c) ((double)(((c) >> 16) & 0xff) / 255.0) +#define GREEN(c) ((double)(((c) >> 8) & 0xff) / 255.0) +#define BLUE(c) ((double)(((c)) & 0xff) / 255.0) + +CColor::CColor() {} + +CColor::CColor(float r, float g, float b, float a) { + this->r = r; + this->g = g; + this->b = b; + this->a = a; +} + +CColor::CColor(uint64_t hex) { + this->r = RED(hex); + this->g = GREEN(hex); + this->b = BLUE(hex); + this->a = ALPHA(hex); +} + +uint64_t CColor::getAsHex() { + return ((int)a) * 0x1000000 + ((int)r) * 0x10000 + ((int)g) * 0x100 + ((int)b) * 0x1; +} \ No newline at end of file diff --git a/src/helpers/Color.hpp b/src/helpers/Color.hpp new file mode 100644 index 0000000..98ffa47 --- /dev/null +++ b/src/helpers/Color.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +class CColor { + public: + CColor(); + CColor(float r, float g, float b, float a); + CColor(uint64_t); + + float r = 0, g = 0, b = 0, a = 1.f; + + uint64_t getAsHex(); + + CColor operator-(const CColor& c2) const { + return CColor(r - c2.r, g - c2.g, b - c2.b, a - c2.a); + } + + CColor operator+(const CColor& c2) const { + return CColor(r + c2.r, g + c2.g, b + c2.b, a + c2.a); + } + + CColor operator*(const float& v) const { + return CColor(r * v, g * v, b * v, a * v); + } + + bool operator==(const CColor& c2) const { + return r == c2.r && g == c2.g && b == c2.b && a == c2.a; + } + + CColor stripA() const { + return {r, g, b, 1}; + } +}; diff --git a/src/helpers/Log.hpp b/src/helpers/Log.hpp new file mode 100644 index 0000000..e11da90 --- /dev/null +++ b/src/helpers/Log.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include + +enum eLogLevel { + TRACE = 0, + INFO, + LOG, + WARN, + ERR, + CRIT, + NONE +}; + +#define RASSERT(expr, reason, ...) \ + if (!(expr)) { \ + Debug::log(CRIT, "\n==========================================================================================\nASSERTION FAILED! \n\n{}\n\nat: line {} in {}", \ + std::format(reason, ##__VA_ARGS__), __LINE__, \ + ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find_last_of('/') + 1); })().c_str()); \ + printf("Assertion failed! See the log in /tmp/hypr/hyprland.log for more info."); \ + *((int*)nullptr) = 1; /* so that we crash and get a coredump */ \ + } + +#define ASSERT(expr) RASSERT(expr, "?") + +namespace Debug { + inline bool quiet = false; + inline bool verbose = false; + + template + void log(eLogLevel level, const std::string& fmt, Args&&... args) { + + if (!verbose && level == TRACE) + return; + + if (quiet) + return; + + if (level != NONE) { + std::cout << '['; + + switch (level) { + case TRACE: std::cout << "TRACE"; break; + case INFO: std::cout << "INFO"; break; + case LOG: std::cout << "LOG"; break; + case WARN: std::cout << "WARN"; break; + case ERR: std::cout << "ERR"; break; + case CRIT: std::cout << "CRITICAL"; break; + default: break; + } + + std::cout << "] "; + } + + std::cout << std::vformat(fmt, std::make_format_args(args...)) << "\n"; + } +}; \ No newline at end of file diff --git a/src/helpers/Vector2D.cpp b/src/helpers/Vector2D.cpp new file mode 100644 index 0000000..4cd3476 --- /dev/null +++ b/src/helpers/Vector2D.cpp @@ -0,0 +1,51 @@ +#include "Vector2D.hpp" +#include +#include + +Vector2D::Vector2D(double xx, double yy) { + x = xx; + y = yy; +} + +Vector2D::Vector2D() { + x = 0; + y = 0; +} + +Vector2D::~Vector2D() {} + +double Vector2D::normalize() { + // get max abs + const auto max = std::abs(x) > std::abs(y) ? std::abs(x) : std::abs(y); + + x /= max; + y /= max; + + return max; +} + +Vector2D Vector2D::floor() const { + return Vector2D(std::floor(x), std::floor(y)); +} + +Vector2D Vector2D::round() const { + return Vector2D(std::round(x), std::round(y)); +} + +Vector2D Vector2D::clamp(const Vector2D& min, const Vector2D& max) const { + return Vector2D(std::clamp(this->x, min.x, max.x < min.x ? INFINITY : max.x), std::clamp(this->y, min.y, max.y < min.y ? INFINITY : max.y)); +} + +double Vector2D::distance(const Vector2D& other) const { + double dx = x - other.x; + double dy = y - other.y; + return std::sqrt(dx * dx + dy * dy); +} + +double Vector2D::size() const { + return std::sqrt(x * x + y * y); +} + +Vector2D Vector2D::getComponentMax(const Vector2D& other) const { + return Vector2D(std::max(this->x, other.x), std::max(this->y, other.y)); +} diff --git a/src/helpers/Vector2D.hpp b/src/helpers/Vector2D.hpp new file mode 100644 index 0000000..0c2b9ce --- /dev/null +++ b/src/helpers/Vector2D.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include + +class Vector2D { + public: + Vector2D(double, double); + Vector2D(); + ~Vector2D(); + + double x = 0; + double y = 0; + + // returns the scale + double normalize(); + + Vector2D operator+(const Vector2D& a) const { + return Vector2D(this->x + a.x, this->y + a.y); + } + Vector2D operator-(const Vector2D& a) const { + return Vector2D(this->x - a.x, this->y - a.y); + } + Vector2D operator-() const { + return Vector2D(-this->x, -this->y); + } + Vector2D operator*(const double& a) const { + return Vector2D(this->x * a, this->y * a); + } + Vector2D operator/(const double& a) const { + return Vector2D(this->x / a, this->y / a); + } + + bool operator==(const Vector2D& a) const { + return a.x == x && a.y == y; + } + + bool operator!=(const Vector2D& a) const { + return a.x != x || a.y != y; + } + + Vector2D operator*(const Vector2D& a) const { + return Vector2D(this->x * a.x, this->y * a.y); + } + + Vector2D operator/(const Vector2D& a) const { + return Vector2D(this->x / a.x, this->y / a.y); + } + + bool operator>(const Vector2D& a) const { + return this->x > a.x && this->y > a.y; + } + + bool operator<(const Vector2D& a) const { + return this->x < a.x && this->y < a.y; + } + Vector2D& operator+=(const Vector2D& a) { + this->x += a.x; + this->y += a.y; + return *this; + } + Vector2D& operator-=(const Vector2D& a) { + this->x -= a.x; + this->y -= a.y; + return *this; + } + Vector2D& operator*=(const Vector2D& a) { + this->x *= a.x; + this->y *= a.y; + return *this; + } + Vector2D& operator/=(const Vector2D& a) { + this->x /= a.x; + this->y /= a.y; + return *this; + } + Vector2D& operator*=(const double& a) { + this->x *= a; + this->y *= a; + return *this; + } + Vector2D& operator/=(const double& a) { + this->x /= a; + this->y /= a; + return *this; + } + + double distance(const Vector2D& other) const; + double size() const; + Vector2D clamp(const Vector2D& min, const Vector2D& max = Vector2D{-1, -1}) const; + + Vector2D floor() const; + Vector2D round() const; + + Vector2D getComponentMax(const Vector2D& other) const; +}; + +/** + format specification + - 'j', as json array + - 'X', same as std::format("{}x{}", vec.x, vec.y) + - number, floating point precision, use `0` to format as integer +*/ +// absolutely ridiculous formatter spec parsing +#define FORMAT_PARSE(specs__, type__) \ + template \ + constexpr auto parse(FormatContext& ctx) { \ + auto it = ctx.begin(); \ + for (; it != ctx.end() && *it != '}'; it++) { \ + switch (*it) { specs__ default : throw std::format_error("invalid format specification"); } \ + } \ + return it; \ + } + +#define FORMAT_FLAG(spec__, flag__) \ + case spec__: (flag__) = true; break; + +#define FORMAT_NUMBER(buf__) \ + case '0': \ + case '1': \ + case '2': \ + case '3': \ + case '4': \ + case '5': \ + case '6': \ + case '7': \ + case '8': \ + case '9': (buf__).push_back(*it); break; +template +struct std::formatter : std::formatter { + bool formatJson = false; + bool formatX = false; + std::string precision = ""; + FORMAT_PARSE(FORMAT_FLAG('j', formatJson) // + FORMAT_FLAG('X', formatX) // + FORMAT_NUMBER(precision), + Vector2D) + + template + auto format(const Vector2D& vec, FormatContext& ctx) const { + std::string formatString = precision.empty() ? "{}" : std::format("{{:.{}f}}", precision); + + if (formatJson) + formatString = std::format("[{0}, {0}]", formatString); + else if (formatX) + formatString = std::format("{0}x{0}", formatString); + else + formatString = std::format("[Vector2D: x: {0}, y: {0}]", formatString); + try { + string buf = std::vformat(formatString, std::make_format_args(vec.x, vec.y)); + return std::format_to(ctx.out(), "{}", buf); + } catch (std::format_error& e) { return std::format_to(ctx.out(), "[{}, {}]", vec.x, vec.y); } + } +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..def4a43 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,31 @@ + +#include "config/ConfigManager.hpp" +#include "core/hyprlock.hpp" + +int main(int argc, char **argv, char **envp) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--verbose" || arg == "-v") + Debug::verbose = true; + + else if (arg == "--quiet" || arg == "-q") + Debug::quiet = true; + } + + try { + g_pConfigManager = std::make_unique(); + g_pConfigManager->init(); + } catch (const char *err) { + Debug::log(CRIT, "ConfigManager threw: {}", err); + std::string strerr = err; + if (strerr.contains("File does not exist")) + Debug::log(NONE, " Make sure you have a config."); + return 1; + } + + g_pHyprlock = std::make_unique(); + g_pHyprlock->run(); + + return 0; +} \ No newline at end of file diff --git a/src/renderer/AsyncResourceGatherer.cpp b/src/renderer/AsyncResourceGatherer.cpp new file mode 100644 index 0000000..8a33cde --- /dev/null +++ b/src/renderer/AsyncResourceGatherer.cpp @@ -0,0 +1,92 @@ +#include "AsyncResourceGatherer.hpp" +#include "../config/ConfigManager.hpp" +#include "../core/Egl.hpp" +#include +#include + +CAsyncResourceGatherer::CAsyncResourceGatherer() { + thread = std::thread([this]() { this->gather(); }); + thread.detach(); +} + +SPreloadedAsset* CAsyncResourceGatherer::getAssetByID(const std::string& id) { + for (auto& a : assets) { + if (a.first == id) + return &a.second; + } + + return nullptr; +} + +void CAsyncResourceGatherer::gather() { + const auto CWIDGETS = g_pConfigManager->getWidgetConfigs(); + + g_pEGL->makeCurrent(nullptr); + + // gather resources to preload + // clang-format off + int preloads = std::count_if(CWIDGETS.begin(), CWIDGETS.end(), [](const auto& w) { + return w.type == "background"; + }); + // clang-format on + + progress = 0; + for (auto& c : CWIDGETS) { + if (c.type == "background") { + progress += 1.0 / (preloads + 1.0); + + std::string path = std::any_cast(c.values.at("path")); + std::string id = std::string{"background:"} + path; + + // preload bg img + const auto CAIROISURFACE = cairo_image_surface_create_from_png(path.c_str()); + + const auto CAIRO = cairo_create(CAIROISURFACE); + cairo_scale(CAIRO, 1, 1); + + const auto TARGET = &preloadTargets.emplace_back(CAsyncResourceGatherer::SPreloadTarget{}); + + TARGET->size = {cairo_image_surface_get_width(CAIROISURFACE), cairo_image_surface_get_height(CAIROISURFACE)}; + TARGET->type = TARGET_IMAGE; + TARGET->id = id; + + const auto DATA = cairo_image_surface_get_data(CAIROISURFACE); + TARGET->cairo = CAIRO; + TARGET->cairosurface = CAIROISURFACE; + TARGET->data = DATA; + } + } + + ready = true; +} + +void CAsyncResourceGatherer::apply() { + + for (auto& t : preloadTargets) { + if (t.type == TARGET_IMAGE) { + const auto ASSET = &assets[t.id]; + + const auto CAIROFORMAT = cairo_image_surface_get_format((cairo_surface_t*)t.cairosurface); + const GLint glIFormat = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_RGB32F : GL_RGBA; + const GLint glFormat = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_RGB : GL_RGBA; + const GLint glType = CAIROFORMAT == CAIRO_FORMAT_RGB96F ? GL_FLOAT : GL_UNSIGNED_BYTE; + + ASSET->texture.m_vSize = t.size; + ASSET->texture.allocate(); + + glBindTexture(GL_TEXTURE_2D, ASSET->texture.m_iTexID); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + if (CAIROFORMAT != CAIRO_FORMAT_RGB96F) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); + } + glTexImage2D(GL_TEXTURE_2D, 0, glIFormat, ASSET->texture.m_vSize.x, ASSET->texture.m_vSize.y, 0, glFormat, glType, t.data); + + cairo_destroy((cairo_t*)t.cairo); + cairo_surface_destroy((cairo_surface_t*)t.cairosurface); + } + } + + applied = true; +} \ No newline at end of file diff --git a/src/renderer/AsyncResourceGatherer.hpp b/src/renderer/AsyncResourceGatherer.hpp new file mode 100644 index 0000000..6c0f8a0 --- /dev/null +++ b/src/renderer/AsyncResourceGatherer.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "Shader.hpp" +#include "../helpers/Box.hpp" +#include "../helpers/Color.hpp" +#include "Texture.hpp" +#include +#include +#include +#include + +struct SPreloadedAsset { + CTexture texture; +}; + +class CAsyncResourceGatherer { + public: + CAsyncResourceGatherer(); + std::atomic ready = false; + std::atomic applied = false; + + std::atomic progress = 0; + + SPreloadedAsset* getAssetByID(const std::string& id); + + void apply(); + + private: + std::thread thread; + + enum eTargetType { + TARGET_IMAGE = 0, + }; + + struct SPreloadTarget { + eTargetType type = TARGET_IMAGE; + std::string id = ""; + + void* data; + void* cairo; + void* cairosurface; + + Vector2D size; + }; + + std::vector preloadTargets; + std::unordered_map assets; + + void gather(); +}; \ No newline at end of file diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp new file mode 100644 index 0000000..c6a0cf2 --- /dev/null +++ b/src/renderer/Renderer.cpp @@ -0,0 +1,262 @@ +#include "Renderer.hpp" +#include "../core/Egl.hpp" +#include "../config/ConfigManager.hpp" +#include "../helpers/Color.hpp" +#include "../core/Output.hpp" +#include "mtx.hpp" + +#include +#include + +#include + +#include "Shaders.hpp" + +#include "widgets/PasswordInputField.hpp" +#include "widgets/Background.hpp" + +inline const float fullVerts[] = { + 1, 0, // top right + 0, 0, // top left + 1, 1, // bottom right + 0, 1, // bottom left +}; + +GLuint compileShader(const GLuint& type, std::string src) { + auto shader = glCreateShader(type); + + auto shaderSource = src.c_str(); + + glShaderSource(shader, 1, (const GLchar**)&shaderSource, nullptr); + glCompileShader(shader); + + GLint ok; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + + RASSERT(ok != GL_FALSE, "compileShader() failed! GL_COMPILE_STATUS not OK!"); + + return shader; +} + +GLuint createProgram(const std::string& vert, const std::string& frag) { + auto vertCompiled = compileShader(GL_VERTEX_SHADER, vert); + + RASSERT(vertCompiled, "Compiling shader failed. VERTEX NULL! Shader source:\n\n{}", vert.c_str()); + + auto fragCompiled = compileShader(GL_FRAGMENT_SHADER, frag); + + RASSERT(fragCompiled, "Compiling shader failed. FRAGMENT NULL! Shader source:\n\n{}", frag.c_str()); + + auto prog = glCreateProgram(); + glAttachShader(prog, vertCompiled); + glAttachShader(prog, fragCompiled); + glLinkProgram(prog); + + glDetachShader(prog, vertCompiled); + glDetachShader(prog, fragCompiled); + glDeleteShader(vertCompiled); + glDeleteShader(fragCompiled); + + GLint ok; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + + RASSERT(ok != GL_FALSE, "createProgram() failed! GL_LINK_STATUS not OK!"); + + return prog; +} + +static void glMessageCallbackA(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { + if (type != GL_DEBUG_TYPE_ERROR) + return; + Debug::log(LOG, "[gl] {}", (const char*)message); +} + +CRenderer::CRenderer() { + g_pEGL->makeCurrent(nullptr); + + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageCallback(glMessageCallbackA, 0); + + GLuint prog = createProgram(QUADVERTSRC, QUADFRAGSRC); + rectShader.program = prog; + rectShader.proj = glGetUniformLocation(prog, "proj"); + rectShader.color = glGetUniformLocation(prog, "color"); + rectShader.posAttrib = glGetAttribLocation(prog, "pos"); + rectShader.topLeft = glGetUniformLocation(prog, "topLeft"); + rectShader.fullSize = glGetUniformLocation(prog, "fullSize"); + rectShader.radius = glGetUniformLocation(prog, "radius"); + + prog = createProgram(TEXVERTSRC, TEXFRAGSRCRGBA); + texShader.program = prog; + texShader.proj = glGetUniformLocation(prog, "proj"); + texShader.tex = glGetUniformLocation(prog, "tex"); + texShader.alphaMatte = glGetUniformLocation(prog, "texMatte"); + texShader.alpha = glGetUniformLocation(prog, "alpha"); + texShader.texAttrib = glGetAttribLocation(prog, "texcoord"); + texShader.matteTexAttrib = glGetAttribLocation(prog, "texcoordMatte"); + texShader.posAttrib = glGetAttribLocation(prog, "pos"); + texShader.discardOpaque = glGetUniformLocation(prog, "discardOpaque"); + texShader.discardAlpha = glGetUniformLocation(prog, "discardAlpha"); + texShader.discardAlphaValue = glGetUniformLocation(prog, "discardAlphaValue"); + texShader.topLeft = glGetUniformLocation(prog, "topLeft"); + texShader.fullSize = glGetUniformLocation(prog, "fullSize"); + texShader.radius = glGetUniformLocation(prog, "radius"); + texShader.applyTint = glGetUniformLocation(prog, "applyTint"); + texShader.tint = glGetUniformLocation(prog, "tint"); + texShader.useAlphaMatte = glGetUniformLocation(prog, "useAlphaMatte"); + + wlr_matrix_identity(projMatrix.data()); + + asyncResourceGatherer = std::make_unique(); +} + +static int frames = 0; + +// +CRenderer::SRenderFeedback CRenderer::renderLock(const CSessionLockSurface& surf) { + static auto* const PDISABLEBAR = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("general:disable_loading_bar"); + + matrixProjection(projection.data(), surf.size.x, surf.size.y, WL_OUTPUT_TRANSFORM_NORMAL); + + g_pEGL->makeCurrent(surf.eglSurface); + glViewport(0, 0, surf.size.x, surf.size.y); + + glScissor(frames, 0, surf.size.x, surf.size.y); + + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + float bga = 0; + + SRenderFeedback feedback; + + if (!asyncResourceGatherer->ready) { + // render status + if (!**PDISABLEBAR) { + CBox progress = {0, 0, asyncResourceGatherer->progress * surf.size.x, 2}; + renderRect(progress, CColor{0.2f, 0.1f, 0.1f, 1.f}, 0); + } + } else { + + if (!asyncResourceGatherer->applied) { + asyncResourceGatherer->apply(); + gatheredAt = std::chrono::system_clock::now(); + } + + // render widgets + const auto WIDGETS = getOrCreateWidgetsFor(&surf); + for (auto& w : *WIDGETS) { + feedback.needsFrame = w->draw() || feedback.needsFrame; + } + } + + frames++; + + Debug::log(LOG, "frame {}", frames); + + feedback.needsFrame = feedback.needsFrame || bga < 1.0; + return feedback; +} + +void CRenderer::renderRect(const CBox& box, const CColor& col, int rounding) { + float matrix[9]; + wlr_matrix_project_box(matrix, &box, WL_OUTPUT_TRANSFORM_NORMAL, 0, + projMatrix.data()); // TODO: write own, don't use WLR here + + float glMatrix[9]; + wlr_matrix_multiply(glMatrix, projection.data(), matrix); + + glUseProgram(rectShader.program); + + glUniformMatrix3fv(rectShader.proj, 1, GL_TRUE, glMatrix); + + // premultiply the color as well as we don't work with straight alpha + glUniform4f(rectShader.color, col.r * col.a, col.g * col.a, col.b * col.a, col.a); + + const auto TOPLEFT = Vector2D(box.x, box.y); + const auto FULLSIZE = Vector2D(box.width, box.height); + + // Rounded corners + glUniform2f(rectShader.topLeft, (float)TOPLEFT.x, (float)TOPLEFT.y); + glUniform2f(rectShader.fullSize, (float)FULLSIZE.x, (float)FULLSIZE.y); + glUniform1f(rectShader.radius, rounding); + + glVertexAttribPointer(rectShader.posAttrib, 2, GL_FLOAT, GL_FALSE, 0, fullVerts); + + glEnableVertexAttribArray(rectShader.posAttrib); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + glDisableVertexAttribArray(rectShader.posAttrib); +} + +void CRenderer::renderTexture(const CBox& box, const CTexture& tex, float a, int rounding) { + float matrix[9]; + wlr_matrix_project_box(matrix, &box, WL_OUTPUT_TRANSFORM_FLIPPED_180 /* ugh coordinate spaces */, 0, + projMatrix.data()); // TODO: write own, don't use WLR here + + float glMatrix[9]; + wlr_matrix_multiply(glMatrix, projection.data(), matrix); + + CShader* shader = &texShader; + + glActiveTexture(GL_TEXTURE0); + glBindTexture(tex.m_iTarget, tex.m_iTexID); + + glUseProgram(shader->program); + + glUniformMatrix3fv(shader->proj, 1, GL_TRUE, glMatrix); + glUniform1i(shader->tex, 0); + glUniform1f(shader->alpha, a); + const auto TOPLEFT = Vector2D(box.x, box.y); + const auto FULLSIZE = Vector2D(box.width, box.height); + + // Rounded corners + glUniform2f(shader->topLeft, TOPLEFT.x, TOPLEFT.y); + glUniform2f(shader->fullSize, FULLSIZE.x, FULLSIZE.y); + glUniform1f(shader->radius, rounding); + + glUniform1i(shader->discardOpaque, 0); + glUniform1i(shader->discardAlpha, 0); + glUniform1i(shader->applyTint, 0); + + glVertexAttribPointer(shader->posAttrib, 2, GL_FLOAT, GL_FALSE, 0, fullVerts); + glVertexAttribPointer(shader->texAttrib, 2, GL_FLOAT, GL_FALSE, 0, fullVerts); + + glEnableVertexAttribArray(shader->posAttrib); + glEnableVertexAttribArray(shader->texAttrib); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + glDisableVertexAttribArray(shader->posAttrib); + glDisableVertexAttribArray(shader->texAttrib); + + glBindTexture(tex.m_iTarget, 0); +} + +std::vector>* CRenderer::getOrCreateWidgetsFor(const CSessionLockSurface* surf) { + if (!widgets.contains(surf)) { + + const auto CWIDGETS = g_pConfigManager->getWidgetConfigs(); + + for (auto& c : CWIDGETS) { + if (!c.monitor.empty() && c.monitor != surf->output->stringPort) + continue; + + // by type + if (c.type == "background") + widgets[surf].emplace_back(std::make_unique(surf->size, std::string{"background:"} + std::any_cast(c.values.at("path")))); + if (c.type == "input-field") { + const auto SIZE = std::any_cast(c.values.at("size")); + widgets[surf].emplace_back(std::make_unique(surf->size, Vector2D{SIZE.x, SIZE.y}, std::any_cast(c.values.at("outer_color")), + std::any_cast(c.values.at("inner_color")), + std::any_cast(c.values.at("outline_thickness")))); + } + } + } + + return &widgets[surf]; +} diff --git a/src/renderer/Renderer.hpp b/src/renderer/Renderer.hpp new file mode 100644 index 0000000..e07536b --- /dev/null +++ b/src/renderer/Renderer.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "../core/LockSurface.hpp" +#include "Shader.hpp" +#include "../helpers/Box.hpp" +#include "../helpers/Color.hpp" +#include "AsyncResourceGatherer.hpp" +#include "widgets/IWidget.hpp" + +typedef std::unordered_map>> widgetMap_t; + +class CRenderer { + public: + CRenderer(); + + struct SRenderFeedback { + bool needsFrame = false; + }; + + SRenderFeedback renderLock(const CSessionLockSurface& surface); + + void renderRect(const CBox& box, const CColor& col, int rounding = 0); + void renderTexture(const CBox& box, const CTexture& tex, float a = 1.0, int rounding = 0); + + std::unique_ptr asyncResourceGatherer; + std::chrono::system_clock::time_point gatheredAt; + + private: + widgetMap_t widgets; + + std::vector>* getOrCreateWidgetsFor(const CSessionLockSurface* surf); + + CShader rectShader; + CShader texShader; + + std::array projMatrix; + std::array projection; +}; + +inline std::unique_ptr g_pRenderer; \ No newline at end of file diff --git a/src/renderer/Shader.cpp b/src/renderer/Shader.cpp new file mode 100644 index 0000000..b494d79 --- /dev/null +++ b/src/renderer/Shader.cpp @@ -0,0 +1,23 @@ +#include "Shader.hpp" + +GLint CShader::getUniformLocation(const std::string& unif) { + const auto itpos = m_muUniforms.find(unif); + + if (itpos == m_muUniforms.end()) { + const auto unifLoc = glGetUniformLocation(program, unif.c_str()); + m_muUniforms[unif] = unifLoc; + return unifLoc; + } + + return itpos->second; +} + +CShader::~CShader() { + destroy(); +} + +void CShader::destroy() { + glDeleteProgram(program); + + program = 0; +} \ No newline at end of file diff --git a/src/renderer/Shader.hpp b/src/renderer/Shader.hpp new file mode 100644 index 0000000..e123632 --- /dev/null +++ b/src/renderer/Shader.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +class CShader { + public: + ~CShader(); + + GLuint program = 0; + GLint proj = -1; + GLint color = -1; + GLint alphaMatte = -1; + GLint tex = -1; + GLint alpha = -1; + GLint posAttrib = -1; + GLint texAttrib = -1; + GLint matteTexAttrib = -1; + GLint discardOpaque = -1; + GLint discardAlpha = -1; + GLfloat discardAlphaValue = -1; + + GLint topLeft = -1; + GLint bottomRight = -1; + GLint fullSize = -1; + GLint fullSizeUntransformed = -1; + GLint radius = -1; + GLint radiusOuter = -1; + + GLint thick = -1; + + GLint halfpixel = -1; + + GLint range = -1; + GLint shadowPower = -1; + GLint useAlphaMatte = -1; // always inverted + + GLint applyTint = -1; + GLint tint = -1; + + GLint gradient = -1; + GLint gradientLength = -1; + GLint angle = -1; + + GLint time = -1; + GLint distort = -1; + GLint wl_output = -1; + + // Blur prepare + GLint contrast = -1; + + // Blur + GLint passes = -1; // Used by `vibrancy` + GLint vibrancy = -1; + GLint vibrancy_darkness = -1; + + // Blur finish + GLint brightness = -1; + GLint noise = -1; + + GLint getUniformLocation(const std::string&); + + void destroy(); + + private: + std::unordered_map m_muUniforms; +}; \ No newline at end of file diff --git a/src/renderer/Shaders.hpp b/src/renderer/Shaders.hpp new file mode 100644 index 0000000..0eca6a9 --- /dev/null +++ b/src/renderer/Shaders.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include + +inline static constexpr auto ROUNDED_SHADER_FUNC = [](const std::string colorVarName) -> std::string { + return R"#( + + // branchless baby! + highp vec2 pixCoord = vec2(gl_FragCoord); + pixCoord -= topLeft + fullSize * 0.5; + pixCoord *= vec2(lessThan(pixCoord, vec2(0.0))) * -2.0 + 1.0; + pixCoord -= fullSize * 0.5 - radius; + pixCoord += vec2(1.0, 1.0) / fullSize; // center the pix dont make it top-left + + if (pixCoord.x + pixCoord.y > radius) { + + float dist = length(pixCoord); + + if (dist > radius + 1.0) + discard; + + if (dist > radius - 1.0) { + float dist = length(pixCoord); + + float normalized = 1.0 - smoothstep(0.0, 1.0, dist - radius + 0.5); + + )#" + + colorVarName + R"#( = )#" + colorVarName + R"#( * normalized; + } + + } +)#"; +}; + +inline const std::string QUADVERTSRC = R"#( +uniform mat3 proj; +uniform vec4 color; +attribute vec2 pos; +attribute vec2 texcoord; +attribute vec2 texcoordMatte; +varying vec4 v_color; +varying vec2 v_texcoord; +varying vec2 v_texcoordMatte; + +void main() { + gl_Position = vec4(proj * vec3(pos, 1.0), 1.0); + v_color = color; + v_texcoord = texcoord; + v_texcoordMatte = texcoordMatte; +})#"; + +inline const std::string QUADFRAGSRC = R"#( +precision highp float; +varying vec4 v_color; + +uniform vec2 topLeft; +uniform vec2 fullSize; +uniform float radius; + +void main() { + + vec4 pixColor = v_color; + + if (radius > 0.0) { + )#" + + ROUNDED_SHADER_FUNC("pixColor") + R"#( + } + + gl_FragColor = pixColor; +})#"; + +inline const std::string TEXVERTSRC = R"#( +uniform mat3 proj; +attribute vec2 pos; +attribute vec2 texcoord; +varying vec2 v_texcoord; + +void main() { + gl_Position = vec4(proj * vec3(pos, 1.0), 1.0); + v_texcoord = texcoord; +})#"; + +inline const std::string TEXFRAGSRCRGBA = R"#( +precision highp float; +varying vec2 v_texcoord; // is in 0-1 +uniform sampler2D tex; +uniform float alpha; + +uniform vec2 topLeft; +uniform vec2 fullSize; +uniform float radius; + +uniform int discardOpaque; +uniform int discardAlpha; +uniform float discardAlphaValue; + +uniform int applyTint; +uniform vec3 tint; + +void main() { + + vec4 pixColor = texture2D(tex, v_texcoord); + + if (discardOpaque == 1 && pixColor[3] * alpha == 1.0) + discard; + + if (discardAlpha == 1 && pixColor[3] <= discardAlphaValue) + discard; + + if (applyTint == 1) { + pixColor[0] = pixColor[0] * tint[0]; + pixColor[1] = pixColor[1] * tint[1]; + pixColor[2] = pixColor[2] * tint[2]; + } + + if (radius > 0.0) { + )#" + + ROUNDED_SHADER_FUNC("pixColor") + R"#( + } + + gl_FragColor = pixColor * alpha; +})#"; \ No newline at end of file diff --git a/src/renderer/Texture.cpp b/src/renderer/Texture.cpp new file mode 100644 index 0000000..96b4987 --- /dev/null +++ b/src/renderer/Texture.cpp @@ -0,0 +1,19 @@ +#include "Texture.hpp" + +CTexture::CTexture() { + // naffin' +} + +void CTexture::destroyTexture() { + if (m_bAllocated) { + glDeleteTextures(1, &m_iTexID); + m_iTexID = 0; + } + m_bAllocated = false; +} + +void CTexture::allocate() { + if (!m_bAllocated) + glGenTextures(1, &m_iTexID); + m_bAllocated = true; +} \ No newline at end of file diff --git a/src/renderer/Texture.hpp b/src/renderer/Texture.hpp new file mode 100644 index 0000000..56f0e67 --- /dev/null +++ b/src/renderer/Texture.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "../helpers/Vector2D.hpp" + +enum TEXTURETYPE { + TEXTURE_INVALID, // Invalid + TEXTURE_RGBA, // 4 channels + TEXTURE_RGBX, // discard A + TEXTURE_EXTERNAL, // EGLImage +}; + +class CTexture { + public: + CTexture(); + + void destroyTexture(); + void allocate(); + + TEXTURETYPE m_iType = TEXTURE_RGBA; + GLenum m_iTarget = GL_TEXTURE_2D; + bool m_bAllocated = false; + GLuint m_iTexID = 0; + Vector2D m_vSize; +}; \ No newline at end of file diff --git a/src/renderer/mtx.hpp b/src/renderer/mtx.hpp new file mode 100644 index 0000000..372d6bc --- /dev/null +++ b/src/renderer/mtx.hpp @@ -0,0 +1,236 @@ + +#pragma once +#include +#include +#include "../helpers/Box.hpp" + +enum wl_output_transform wlr_output_transform_invert(enum wl_output_transform tr) { + // if ((tr & WL_OUTPUT_TRANSFORM_90) && !(tr & WL_OUTPUT_TRANSFORM_FLIPPED)) { + // tr ^= WL_OUTPUT_TRANSFORM_180; + // } + return tr; +} + +void wlr_matrix_identity(float mat[9]) { + const float identity[9] = { + 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, + }; + memcpy(mat, identity, sizeof(identity)); +} + +void wlr_matrix_multiply(float mat[9], const float a[9], const float b[9]) { + float product[9]; + + product[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6]; + product[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7]; + product[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8]; + + product[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6]; + product[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7]; + product[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8]; + + product[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6]; + product[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7]; + product[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8]; + + memcpy(mat, product, sizeof(product)); +} + +void wlr_matrix_transpose(float mat[9], const float a[9]) { + float transposition[9] = { + a[0], a[3], a[6], a[1], a[4], a[7], a[2], a[5], a[8], + }; + memcpy(mat, transposition, sizeof(transposition)); +} + +void wlr_matrix_translate(float mat[9], float x, float y) { + float translate[9] = { + 1.0f, 0.0f, x, 0.0f, 1.0f, y, 0.0f, 0.0f, 1.0f, + }; + wlr_matrix_multiply(mat, mat, translate); +} + +void wlr_matrix_scale(float mat[9], float x, float y) { + float scale[9] = { + x, 0.0f, 0.0f, 0.0f, y, 0.0f, 0.0f, 0.0f, 1.0f, + }; + wlr_matrix_multiply(mat, mat, scale); +} + +void wlr_matrix_rotate(float mat[9], float rad) { + float rotate[9] = { + cos(rad), -sin(rad), 0.0f, sin(rad), cos(rad), 0.0f, 0.0f, 0.0f, 1.0f, + }; + wlr_matrix_multiply(mat, mat, rotate); +} + +const float transforms[][9] = { + [WL_OUTPUT_TRANSFORM_NORMAL] = + { + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_90] = + { + 0.0f, + 1.0f, + 0.0f, + -1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_180] = + { + -1.0f, + 0.0f, + 0.0f, + 0.0f, + -1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_270] = + { + 0.0f, + -1.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_FLIPPED] = + { + -1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_FLIPPED_90] = + { + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_FLIPPED_180] = + { + 1.0f, + 0.0f, + 0.0f, + 0.0f, + -1.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, + [WL_OUTPUT_TRANSFORM_FLIPPED_270] = + { + 0.0f, + -1.0f, + 0.0f, + -1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }, +}; + +void wlr_matrix_transform(float mat[9], enum wl_output_transform transform) { + wlr_matrix_multiply(mat, mat, transforms[transform]); +} + +void matrix_projection(float mat[9], int width, int height, enum wl_output_transform transform) { + std::memset(mat, 0, sizeof(*mat) * 9); + + const float* t = transforms[transform]; + float x = 2.0f / width; + float y = 2.0f / height; + + // Rotation + reflection + mat[0] = x * t[0]; + mat[1] = x * t[1]; + mat[3] = y * -t[3]; + mat[4] = y * -t[4]; + + // Translation + mat[2] = -copysign(1.0f, mat[0] + mat[1]); + mat[5] = -copysign(1.0f, mat[3] + mat[4]); + + // Identity + mat[8] = 1.0f; +} + +void wlr_matrix_project_box(float mat[9], const CBox* box, enum wl_output_transform transform, float rotation, const float projection[9]) { + int x = box->x; + int y = box->y; + int width = box->width; + int height = box->height; + + wlr_matrix_identity(mat); + wlr_matrix_translate(mat, x, y); + + if (rotation != 0) { + wlr_matrix_translate(mat, width / 2, height / 2); + wlr_matrix_rotate(mat, rotation); + wlr_matrix_translate(mat, -width / 2, -height / 2); + } + + wlr_matrix_scale(mat, width, height); + + if (transform != WL_OUTPUT_TRANSFORM_NORMAL) { + wlr_matrix_translate(mat, 0.5, 0.5); + wlr_matrix_transform(mat, transform); + wlr_matrix_translate(mat, -0.5, -0.5); + } + + wlr_matrix_multiply(mat, projection, mat); +} + +void matrixProjection(float mat[9], int w, int h, wl_output_transform tr) { + memset(mat, 0, sizeof(*mat) * 9); + + const float* t = transforms[tr]; + float x = 2.0f / w; + float y = 2.0f / h; + + // Rotation + reflection + mat[0] = x * t[0]; + mat[1] = x * t[1]; + mat[3] = y * t[3]; + mat[4] = y * t[4]; + + // Translation + mat[2] = -copysign(1.0f, mat[0] + mat[1]); + mat[5] = -copysign(1.0f, mat[3] + mat[4]); + + // Identity + mat[8] = 1.0f; +} \ No newline at end of file diff --git a/src/renderer/widgets/Background.cpp b/src/renderer/widgets/Background.cpp new file mode 100644 index 0000000..413e728 --- /dev/null +++ b/src/renderer/widgets/Background.cpp @@ -0,0 +1,21 @@ +#include "Background.hpp" +#include "../Renderer.hpp" + +CBackground::CBackground(const Vector2D& viewport_, const std::string& resourceID_) : viewport(viewport_), resourceID(resourceID_) { + ; +} + +bool CBackground::draw() { + if (!asset) + asset = g_pRenderer->asyncResourceGatherer->getAssetByID(resourceID); + + if (!asset) + return false; + + float bga = std::clamp(std::chrono::duration_cast(std::chrono::system_clock::now() - g_pRenderer->gatheredAt).count() / 500000.0, 0.0, 1.0); + CBox monbox = {0, 0, viewport.x, viewport.y}; + + g_pRenderer->renderTexture(monbox, asset->texture, bga); + + return bga < 1.0; +} \ No newline at end of file diff --git a/src/renderer/widgets/Background.hpp b/src/renderer/widgets/Background.hpp new file mode 100644 index 0000000..a99aa1d --- /dev/null +++ b/src/renderer/widgets/Background.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "IWidget.hpp" +#include "../../helpers/Vector2D.hpp" +#include + +struct SPreloadedAsset; + +class CBackground : public IWidget { + public: + CBackground(const Vector2D& viewport, const std::string& resourceID); + + virtual bool draw(); + + private: + Vector2D viewport; + std::string resourceID; + SPreloadedAsset* asset = nullptr; +}; \ No newline at end of file diff --git a/src/renderer/widgets/IWidget.hpp b/src/renderer/widgets/IWidget.hpp new file mode 100644 index 0000000..1f667dd --- /dev/null +++ b/src/renderer/widgets/IWidget.hpp @@ -0,0 +1,6 @@ +#pragma once + +class IWidget { + public: + virtual bool draw() = 0; +}; \ No newline at end of file diff --git a/src/renderer/widgets/PasswordInputField.cpp b/src/renderer/widgets/PasswordInputField.cpp new file mode 100644 index 0000000..1b7dbbc --- /dev/null +++ b/src/renderer/widgets/PasswordInputField.cpp @@ -0,0 +1,62 @@ +#include "PasswordInputField.hpp" +#include "../Renderer.hpp" +#include "../../core/hyprlock.hpp" +#include + +CPasswordInputField::CPasswordInputField(const Vector2D& viewport, const Vector2D& size_, const CColor& outer_, const CColor& inner_, int out_thick_) { + size = size_; + pos = viewport / 2.f - size_ / 2.f; + inner = inner_; + outer = outer_; + out_thick = out_thick_; +} + +void CPasswordInputField::updateDots() { + const auto PASSLEN = g_pHyprlock->getPasswordBufferLen(); + + size_t dotsAppearingOrPresent = std::count_if(dots.begin(), dots.end(), [](const auto& dot) { return dot.appearing || !dot.animated; }); + + if (dotsAppearingOrPresent < PASSLEN) { + dots.push_back(dot{.idx = dotsAppearingOrPresent + 1, .appearing = true, .animated = true, .a = 0, .start = std::chrono::system_clock::now()}); + } else if (dotsAppearingOrPresent > PASSLEN) { + dots[dots.size() - 1].animated = true; + dots[dots.size() - 1].appearing = false; + dots[dots.size() - 1].start = std::chrono::system_clock::now(); + } + + for (auto& dot : dots) { + if (dot.appearing) { + if (dot.a < 1.0) + dot.a = std::clamp(std::chrono::duration_cast(std::chrono::system_clock::now() - dot.start).count() / 100000.0, 0.0, 1.0); + } else { + if (dot.a > 0.0) + dot.a = std::clamp(1.0 - std::chrono::duration_cast(std::chrono::system_clock::now() - dot.start).count() / 100000.0, 0.0, 1.0); + } + + if (dot.appearing && dot.a == 1.0) + dot.animated = false; + } + + std::erase_if(dots, [](const auto& dot) { return !dot.appearing && dot.a == 0.0; }); +} + +bool CPasswordInputField::draw() { + CBox inputFieldBox = {pos, size}; + CBox outerBox = {pos - Vector2D{out_thick, out_thick}, size + Vector2D{out_thick * 2, out_thick * 2}}; + + updateDots(); + + g_pRenderer->renderRect(outerBox, outer, outerBox.h / 2.0); + g_pRenderer->renderRect(inputFieldBox, inner, inputFieldBox.h / 2.0); + + constexpr int PASS_SPACING = 3; + constexpr int PASS_SIZE = 8; + + for (size_t i = 0; i < dots.size(); ++i) { + Vector2D currentPos = inputFieldBox.pos() + Vector2D{PASS_SPACING, inputFieldBox.h / 2.f - PASS_SIZE / 2.f} + Vector2D{(PASS_SIZE + PASS_SPACING) * dots[i].idx, 0}; + CBox box{currentPos, Vector2D{PASS_SIZE, PASS_SIZE}}; + g_pRenderer->renderRect(box, CColor{0, 0, 0, dots[i].a}, PASS_SIZE / 2.0); + } + + return std::ranges::any_of(dots.begin(), dots.end(), [](const auto& dot) { return dot.animated; }); +} \ No newline at end of file diff --git a/src/renderer/widgets/PasswordInputField.hpp b/src/renderer/widgets/PasswordInputField.hpp new file mode 100644 index 0000000..eb98968 --- /dev/null +++ b/src/renderer/widgets/PasswordInputField.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "IWidget.hpp" +#include "../../helpers/Vector2D.hpp" +#include "../../helpers/Color.hpp" +#include +#include + +class CPasswordInputField : public IWidget { + public: + CPasswordInputField(const Vector2D& viewport, const Vector2D& size, const CColor& outer, const CColor& inner, int out_thick); + + virtual bool draw(); + + private: + void updateDots(); + + Vector2D size; + Vector2D pos; + + int out_thick; + + CColor inner, outer; + + struct dot { + size_t idx = 0; + bool appearing = false; + bool animated = false; + float a = 0; + std::chrono::system_clock::time_point start; + }; + + std::vector dots; +}; \ No newline at end of file