From 6e86ff41dba603510f6c5147a292f82173b2c4fa Mon Sep 17 00:00:00 2001 From: Slendi Date: Sat, 4 Oct 2025 23:12:01 +0300 Subject: [PATCH] Initial commit Signed-off-by: Slendi --- CMakeLists.txt | 166 ++++++++--------- blur.xml | 28 +++ flake.nix | 5 +- main.cpp | 200 --------------------- src/Theme.hpp | 35 ++++ src/enum_array.hpp | 73 ++++++++ src/main.cpp | 432 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 645 insertions(+), 294 deletions(-) create mode 100644 blur.xml delete mode 100644 main.cpp create mode 100644 src/Theme.hpp create mode 100644 src/enum_array.hpp create mode 100644 src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index efa8a53..e9c0f62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,32 +1,37 @@ cmake_minimum_required(VERSION 3.16) -project(skia_wayland_nodeco LANGUAGES C CXX) +project(waylight LANGUAGES C CXX) -# --- toolchain & opts --------------------------------------------------------- set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# --- deps (pkg-config) -------------------------------------------------------- find_package(PkgConfig REQUIRED) -pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client) -pkg_check_modules(WAYLAND_EGL REQUIRED wayland-egl) -pkg_check_modules(EGL REQUIRED egl) -pkg_check_modules(GLES2 REQUIRED glesv2) -# Skia package name varies; this works on many distros. Adjust if needed. -pkg_check_modules(SKIA REQUIRED skia) -# The protocol XMLs live here: +pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) +pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) +pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl) +pkg_check_modules(GLES2 REQUIRED IMPORTED_TARGET glesv2) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(LIBPORTAL REQUIRED IMPORTED_TARGET libportal) pkg_check_modules(WAYLAND_PROTOCOLS REQUIRED wayland-protocols) +pkg_check_modules(WLR_PROTOCOLS REQUIRED wlr-protocols) + +include(FetchContent) + +FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/slendidev/raylib.git + GIT_TAG "lunar" + GIT_SHALLOW 1 +) +set(OPENGL_VERSION "ES 2.0") +set(PLATFORM DRM) +set(BUILD_EXAMPLES OFF) +FetchContent_MakeAvailable(raylib) -# --- scanners ----------------------------------------------------------------- find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) -# waylandpp's generator; provided by the waylandpp package -# (typically installed as 'wayland-scanner++') -find_program(WAYLAND_SCANNER_PP wayland-scanner++ REQUIRED) -# --- protocol XML paths ------------------------------------------------------- pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) -# fallback via datarootdir if needed if(NOT WAYLAND_PROTOCOLS_DIR OR NOT EXISTS "${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml") pkg_get_variable(_WP_DATAROOT wayland-protocols datarootdir) if(_WP_DATAROOT AND EXISTS "${_WP_DATAROOT}/wayland-protocols") @@ -34,7 +39,6 @@ if(NOT WAYLAND_PROTOCOLS_DIR OR NOT EXISTS "${WAYLAND_PROTOCOLS_DIR}/stable/xdg- endif() endif() -# last-resort search (covers odd layouts) if(NOT WAYLAND_PROTOCOLS_DIR OR NOT EXISTS "${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml") find_path(WAYLAND_PROTOCOLS_DIR NAMES stable/xdg-shell/xdg-shell.xml @@ -50,114 +54,92 @@ endif() set(XDG_SHELL_XML "${WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml" ) -set(XDG_DECOR_XML - "${WAYLAND_PROTOCOLS_DIR}/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" -) - -# --- generated outputs (into build dir) --------------------------------------- set(GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") file(MAKE_DIRECTORY "${GEN_DIR}") -# waylandpp C++ headers + code (used by the app) -set(GEN_CXX_HEADERS - "${GEN_DIR}/xdg-shell-client-protocol.hpp" - "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.hpp" -) -set(GEN_CXX_SOURCES - "${GEN_DIR}/xdg-shell-client-protocol.cpp" - "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.cpp" +pkg_get_variable(WLR_PROTOCOLS_DIR wlr-protocols pkgdatadir) +if(NOT WLR_PROTOCOLS_DIR OR NOT EXISTS "${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml") + find_path(WLR_PROTOCOLS_DIR + NAMES unstable/wlr-layer-shell-unstable-v1.xml + HINTS ${CMAKE_SYSTEM_PREFIX_PATH} + PATH_SUFFIXES wlr-protocols share/wlr-protocols + ) +endif() +if(NOT WLR_PROTOCOLS_DIR) + message(FATAL_ERROR "Could not locate wlr-protocols datadir (install wlr-protocols?)") +endif() + +set(BACKGROUND_EFFECT_XML + "${WAYLAND_PROTOCOLS_DIR}/staging/ext-background-effect/ext-background-effect-v1.xml" +) + +set(WLR_LAYER_SHELL_XML + "${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml" ) -# (optional) C headers + private impls (not linked, but generated) set(GEN_C_HEADERS "${GEN_DIR}/xdg-shell-client-protocol.h" - "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.h" + "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" + "${GEN_DIR}/ext-background-effect-v1-client-protocol.h" + "${GEN_DIR}/blur-client-protocol.h" ) set(GEN_C_PRIVATES "${GEN_DIR}/xdg-shell-protocol.c" - "${GEN_DIR}/xdg-decoration-unstable-v1-protocol.c" + "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" + "${GEN_DIR}/ext-background-effect-v1-protocol.c" + "${GEN_DIR}/blur-protocol.c" ) -# --- custom commands: generate C++ (required) --------------------------------- -add_custom_command( - OUTPUT ${GEN_CXX_HEADERS} ${GEN_CXX_SOURCES} - COMMAND "${WAYLAND_SCANNER_PP}" "${XDG_SHELL_XML}" "${GEN_DIR}/xdg-shell-client-protocol.hpp" "${GEN_DIR}/xdg-shell-client-protocol.cpp" - COMMAND "${WAYLAND_SCANNER_PP}" "${XDG_DECOR_XML}" "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.hpp" "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.cpp" - DEPENDS "${XDG_SHELL_XML}" "${XDG_DECOR_XML}" - COMMENT "Generating waylandpp C++ protocol headers/sources with wayland-scanner++" - VERBATIM -) - -# --- custom commands: generate C (optional) ----------------------------------- add_custom_command( OUTPUT ${GEN_C_HEADERS} ${GEN_C_PRIVATES} COMMAND "${WAYLAND_SCANNER}" client-header "${XDG_SHELL_XML}" "${GEN_DIR}/xdg-shell-client-protocol.h" COMMAND "${WAYLAND_SCANNER}" private-code "${XDG_SHELL_XML}" "${GEN_DIR}/xdg-shell-protocol.c" - COMMAND "${WAYLAND_SCANNER}" client-header "${XDG_DECOR_XML}" "${GEN_DIR}/xdg-decoration-unstable-v1-client-protocol.h" - COMMAND "${WAYLAND_SCANNER}" private-code "${XDG_DECOR_XML}" "${GEN_DIR}/xdg-decoration-unstable-v1-protocol.c" - DEPENDS "${XDG_SHELL_XML}" "${XDG_DECOR_XML}" - COMMENT "Generating C client headers + private code with wayland-scanner (optional)" + # ext-background-effect + COMMAND "${WAYLAND_SCANNER}" client-header "${BACKGROUND_EFFECT_XML}" "${GEN_DIR}/ext-background-effect-v1-client-protocol.h" + COMMAND "${WAYLAND_SCANNER}" private-code "${BACKGROUND_EFFECT_XML}" "${GEN_DIR}/ext-background-effect-v1-protocol.c" + # wlr-layer-shell + COMMAND "${WAYLAND_SCANNER}" client-header "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-client-protocol.h" + COMMAND "${WAYLAND_SCANNER}" private-code "${WLR_LAYER_SHELL_XML}" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" + # org-kde-win-blur + COMMAND "${WAYLAND_SCANNER}" client-header "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-client-protocol.h" + COMMAND "${WAYLAND_SCANNER}" private-code "${CMAKE_CURRENT_SOURCE_DIR}/blur.xml" "${GEN_DIR}/blur-protocol.c" + DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" + COMMENT "Generating Wayland + wlr-layer-shell client headers and private code" VERBATIM ) add_custom_target(generate_protocols ALL - DEPENDS ${GEN_CXX_HEADERS} ${GEN_CXX_SOURCES} ${GEN_C_HEADERS} ${GEN_C_PRIVATES} + DEPENDS ${GEN_C_HEADERS} ${GEN_C_PRIVATES} ) -# --- executable --------------------------------------------------------------- -add_executable(skia_wayland - ${GEN_CXX_HEADERS} - ${GEN_CXX_SOURCES} +add_executable(waylight + ${GEN_C_PRIVATES} - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp ) -add_dependencies(skia_wayland generate_protocols) +add_dependencies(waylight generate_protocols) -# include dirs (system + generated) -target_include_directories(skia_wayland PRIVATE - ${WAYLAND_CLIENT_INCLUDE_DIRS} - ${WAYLAND_EGL_INCLUDE_DIRS} - ${EGL_INCLUDE_DIRS} - ${GLES2_INCLUDE_DIRS} - ${SKIA_INCLUDE_DIRS} - # waylandpp headers (wayland-client.hpp). If they aren't system-wide, add your path here: - # ${CMAKE_CURRENT_SOURCE_DIR}/external/waylandpp/include +target_include_directories(waylight PRIVATE ${GEN_DIR} ) -# libs -target_link_libraries(skia_wayland PRIVATE - ${WAYLAND_CLIENT_LIBRARIES} - ${WAYLAND_EGL_LIBRARIES} - ${EGL_LIBRARIES} - ${GLES2_LIBRARIES} - ${SKIA_LIBRARIES} - # some distros need these as well: +target_link_libraries(waylight PRIVATE + PkgConfig::WAYLAND_CLIENT + PkgConfig::WAYLAND_EGL + PkgConfig::EGL + PkgConfig::GLES2 + PkgConfig::GLIB + PkgConfig::LIBPORTAL + + raylib + m dl pthread ) -# cflags/ldflags from pkg-config -target_compile_options(skia_wayland PRIVATE - ${WAYLAND_CLIENT_CFLAGS_OTHER} - ${WAYLAND_EGL_CFLAGS_OTHER} - ${EGL_CFLAGS_OTHER} - ${GLES2_CFLAGS_OTHER} - ${SKIA_CFLAGS_OTHER} -) -target_link_options(skia_wayland PRIVATE - ${WAYLAND_CLIENT_LDFLAGS_OTHER} - ${WAYLAND_EGL_LDFLAGS_OTHER} - ${EGL_LDFLAGS_OTHER} - ${GLES2_LDFLAGS_OTHER} - ${SKIA_LDFLAGS_OTHER} -) - -# --- warnings ----------------------------------------------------------------- if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(skia_wayland PRIVATE -Wall -Wextra -Wno-unused-parameter) + target_compile_options(waylight PRIVATE -Wall -Wextra -Wno-unused-parameter) endif() -# --- install (optional) ------------------------------------------------------- -install(TARGETS skia_wayland RUNTIME DESTINATION bin) +install(TARGETS waylight RUNTIME DESTINATION bin) diff --git a/blur.xml b/blur.xml new file mode 100644 index 0000000..477bd72 --- /dev/null +++ b/blur.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flake.nix b/flake.nix index 69f5fd7..642f27e 100644 --- a/flake.nix +++ b/flake.nix @@ -37,10 +37,11 @@ pkg-config wayland wayland-protocols + wlr-protocols wayland-scanner - waylandpp libGL - skia + libportal + glib ] ++ buildInputs ++ nativeBuildInputs diff --git a/main.cpp b/main.cpp deleted file mode 100644 index cf0058e..0000000 --- a/main.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#include "xdg-shell-client-protocol.hpp" - -#include "xdg-decoration-unstable-v1-client-protocol.hpp" - -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -struct App { - wayland::display_t display; - wayland::registry_t registry; - wayland::compositor_t compositor; - wayland::xdg_wm_base_t xdg_wm; - wayland::zxdg_decoration_manager_v1_t dec_mgr; - wayland::surface_t wl_surface; - wayland::xdg_surface_t xdg_surface; - wayland::xdg_toplevel_t xdg_toplevel; - - EGLDisplay edpy = EGL_NO_DISPLAY; - EGLConfig ecfg = nullptr; - EGLContext ectx = EGL_NO_CONTEXT; - EGLSurface esurf = EGL_NO_SURFACE; - wl_egl_window *wegl = nullptr; - - sk_sp gl_iface; - sk_sp gr_ctx; - sk_sp sk_surface; - - int win_w = 800, win_h = 600; - bool running = true; - - void init_wayland() { - registry = display.get_registry(); - registry.on_global() = [&](uint32_t name, const std::string &iface, - uint32_t ver) { - if (iface == wayland::compositor_t::interface_name) - compositor = registry.bind( - name, std::min(ver, 4)); - else if (iface == wayland::xdg_wm_base_t::interface_name) - xdg_wm = registry.bind(name, 1); - else if (iface == wayland::zxdg_decoration_manager_v1_t::interface_name) - dec_mgr = registry.bind(name, 1); - }; - xdg_wm.on_ping() = [&](uint32_t serial) { xdg_wm.pong(serial); }; - display.roundtrip(); - if (!compositor || !xdg_wm) { - fprintf(stderr, "missing compositor\n"); - exit(1); - } - - wl_surface = compositor.create_surface(); - xdg_surface = xdg_wm.get_xdg_surface(wl_surface); - xdg_toplevel = xdg_surface.get_toplevel(); - - if (dec_mgr) { - auto deco = dec_mgr.get_toplevel_decoration(xdg_toplevel); - deco.set_mode(wayland::zxdg_toplevel_decoration_v1_mode::client_side); - } - xdg_toplevel.set_title("Skia Hello World"); - xdg_toplevel.on_close() = [&] { running = false; }; - - xdg_surface.on_configure() = [&](uint32_t serial) { - xdg_surface.ack_configure(serial); - wl_surface.commit(); - }; - xdg_toplevel.on_configure() = [&](int32_t w, int32_t h, - std::vector) { - if (w > 0 && h > 0 && (w != win_w || h != win_h)) { - win_w = w; - win_h = h; - if (wegl) { - wl_egl_window_resize(wegl, win_w, win_h, 0, 0); - recreate_skia_surface(); - } - } - }; - - wl_surface.commit(); - display.roundtrip(); - } - - void init_egl() { - edpy = eglGetDisplay((EGLNativeDisplayType)display.c_ptr()); - eglInitialize(edpy, nullptr, nullptr); - - const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, - EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES2_BIT, - EGL_RED_SIZE, - 8, - EGL_GREEN_SIZE, - 8, - EGL_BLUE_SIZE, - 8, - EGL_ALPHA_SIZE, - 8, - EGL_NONE}; - EGLint n = 0; - eglChooseConfig(edpy, cfgAttribs, &ecfg, 1, &n); - const EGLint ctxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; - ectx = eglCreateContext(edpy, ecfg, EGL_NO_CONTEXT, ctxAttribs); - wegl = wl_egl_window_create(wl_surface, win_w, win_h); - esurf = - eglCreateWindowSurface(edpy, ecfg, (EGLNativeWindowType)wegl, nullptr); - eglMakeCurrent(edpy, esurf, esurf, ectx); - eglSwapInterval(edpy, 1); - } - - void init_skia() { - gl_iface = GrGLMakeNativeInterface(); - gr_ctx = GrDirectContext::MakeGL(gl_iface); - recreate_skia_surface(); - } - - void recreate_skia_surface() { - GrGLFramebufferInfo fbInfo{0, GL_RGBA8}; - GrBackendRenderTarget backendRT(win_w, win_h, 0, 8, fbInfo); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); - sk_surface = SkSurfaces::WrapBackendRenderTarget( - gr_ctx.get(), backendRT, kBottomLeft_GrSurfaceOrigin, - kRGBA_8888_SkColorType, nullptr, &props); - } - - void draw_frame() { - SkCanvas *c = sk_surface->getCanvas(); - c->clear(SkColorSetARGB(255, 20, 22, 26)); - - SkPaint paint; - paint.setAntiAlias(true); - paint.setColor(SkColorSetARGB(255, 240, 240, 255)); - - SkFont font; - font.setSize(48); - - const char *text = "Hello, world!"; - SkRect bounds; - font.measureText(text, strlen(text), SkTextEncoding::kUTF8, &bounds); - - float x = (win_w - bounds.width()) / 2 - bounds.left(); - float y = (win_h + bounds.height()) / 2 - bounds.bottom(); - - c->drawSimpleText(text, strlen(text), SkTextEncoding::kUTF8, x, y, font, - paint); - - sk_surface->flushAndSubmit(); - eglSwapBuffers(edpy, esurf); - } - - void run() { - while (running) { - display.dispatch_pending(); - display.flush(); - draw_frame(); - std::this_thread::sleep_for(std::chrono::milliseconds(16)); - } - } - - void cleanup() { - gr_ctx->flushAndSubmit(); - gr_ctx->releaseResourcesAndAbandonContext(); - sk_surface.reset(); - gl_iface.reset(); - eglMakeCurrent(edpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglDestroySurface(edpy, esurf); - wl_egl_window_destroy(wegl); - eglDestroyContext(edpy, ectx); - eglTerminate(edpy); - } -}; - -int main() { - App a; - a.init_wayland(); - a.init_egl(); - a.init_skia(); - a.run(); - a.cleanup(); - return 0; -} diff --git a/src/Theme.hpp b/src/Theme.hpp new file mode 100644 index 0000000..a76d4c0 --- /dev/null +++ b/src/Theme.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "enum_array.hpp" + +struct ColorScheme { + struct { + Color background; + } window; +}; + +enum class Theme : int { Light = 0, Dark, _last = Dark }; + +template <> struct enum_traits { + static constexpr Theme first = Theme::Light; + static constexpr Theme last = Theme::_last; +}; + +constexpr auto make_default_themes() -> enum_array const { + enum_array array; + array[Theme::Light] = { + .window = + { + .background = {255, 255, 255, 100}, + }, + }; + array[Theme::Dark] = { + .window = + { + .background = {0, 0, 0, 100}, + }, + }; + return array; +} diff --git a/src/enum_array.hpp b/src/enum_array.hpp new file mode 100644 index 0000000..4cc4222 --- /dev/null +++ b/src/enum_array.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +template struct enum_traits; + +template +concept EnumLike = std::is_enum_v; + +template +constexpr std::size_t enum_count_v = + static_cast(enum_traits::last) - + static_cast(enum_traits::first) + 1; + +template struct enum_array { + using value_type = T; + using enum_type = E; + using underlying_index_type = std::size_t; + + static constexpr E first = enum_traits::first; + static constexpr E last = enum_traits::last; + static constexpr std::size_t size_value = enum_count_v; + + std::array _data{}; + + static constexpr std::size_t size() noexcept { return size_value; } + constexpr T *data() noexcept { return _data.data(); } + constexpr const T *data() const noexcept { return _data.data(); } + constexpr T *begin() noexcept { return _data.begin().operator->(); } + constexpr const T *begin() const noexcept { + return _data.begin().operator->(); + } + constexpr T *end() noexcept { return _data.end().operator->(); } + constexpr const T *end() const noexcept { return _data.end().operator->(); } + + constexpr T &operator[](E e) noexcept { return _data[to_index(e)]; } + constexpr const T &operator[](E e) const noexcept { + return _data[to_index(e)]; + } + + constexpr T &at(E e) { + auto i = to_index(e); + if (i >= size_value) + throw std::out_of_range("enum_array::at"); + return _data[i]; + } + constexpr const T &at(E e) const { + auto i = to_index(e); + if (i >= size_value) + throw std::out_of_range("enum_array::at"); + return _data[i]; + } + + constexpr void fill(const T &v) { _data.fill(v); } + +private: + static constexpr std::size_t to_index(E e) noexcept { + return static_cast(e) - static_cast(first); + } +}; + +template + requires EnumLike && (std::is_same_v && ...) +constexpr auto make_enum_array(T &&first_val, U &&...rest) { + enum_array> arr; + static_assert(sizeof...(rest) + 1 == enum_count_v, + "initializer count must match enum range"); + arr._data = {std::forward(first_val), std::forward(rest)...}; + return arr; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4d5b736 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,432 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define namespace namespace_ +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +extern "C" { +#include +} +#undef namespace + +#include "blur-client-protocol.h" +#include "ext-background-effect-v1-client-protocol.h" + +#include + +#include "Theme.hpp" + +struct App { + App() { + init_wayland(); + init_egl(); + init_signal(); + init_theme_portal(); + } + ~App(); + + auto run() -> void { + SetWindowSize(m_win_w, m_win_h); + while (m_running) { + pump_events(); + render_frame(); + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + } + auto set_visible(bool visible) -> void; + auto visible() const -> bool { return m_visible; } + +private: + auto init_wayland() -> void; + auto init_egl() -> void; + auto init_signal() -> void; + auto init_theme_portal() -> void; + auto pump_events() -> void; + auto render_frame() -> void; + auto theme() const -> ColorScheme const & { return m_themes[m_active_theme]; } + + static void on_settings_changed(XdpSettings * /*self*/, const char *ns, + const char *key, GVariant * /*value*/, + gpointer data); + + struct { + wl_display *display{}; + wl_registry *registry{}; + wl_compositor *compositor{}; + wl_surface *wl_surface{}; + zwlr_layer_shell_v1 *layer_shell{}; + zwlr_layer_surface_v1 *layer_surface{}; + ext_background_effect_manager_v1 *mgr{}; + ext_background_effect_surface_v1 *eff{}; + org_kde_kwin_blur_manager *kde_blur_mgr{}; + org_kde_kwin_blur *kde_blur{}; + } m_wayland; + + struct { + EGLDisplay edpy{EGL_NO_DISPLAY}; + EGLConfig ecfg{}; + EGLContext ectx{EGL_NO_CONTEXT}; + EGLSurface esurf{EGL_NO_SURFACE}; + wl_egl_window *wegl{}; + } m_gl; + + struct { + XdpPortal *portal{}; + XdpSettings *settings{}; + } m_xdp; + + enum_array m_themes{make_default_themes()}; + Theme m_active_theme{Theme::Light}; + + int m_win_w{800}; + int m_win_h{600}; + bool m_running{true}; + bool m_visible{true}; + + int m_sfd{-1}; // Signal fd for toggling visibility +}; + +App::~App() { + if (m_sfd != -1) + close(m_sfd); + + if (m_gl.edpy != EGL_NO_DISPLAY) { + eglMakeCurrent(m_gl.edpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (m_gl.esurf != EGL_NO_SURFACE) + eglDestroySurface(m_gl.edpy, m_gl.esurf); + if (m_gl.wegl) + wl_egl_window_destroy(m_gl.wegl); + if (m_gl.ectx != EGL_NO_CONTEXT) + eglDestroyContext(m_gl.edpy, m_gl.ectx); + eglTerminate(m_gl.edpy); + } + + if (m_wayland.wl_surface) + wl_surface_destroy(m_wayland.wl_surface); + if (m_wayland.compositor) + wl_compositor_destroy(m_wayland.compositor); + if (m_wayland.registry) + wl_registry_destroy(m_wayland.registry); + if (m_wayland.display) + wl_display_disconnect(m_wayland.display); + + if (m_xdp.settings) + g_object_unref(m_xdp.settings); + if (m_xdp.portal) + g_object_unref(m_xdp.portal); +} + +auto App::init_wayland() -> void { + m_wayland.display = wl_display_connect(nullptr); + if (!m_wayland.display) { + std::fprintf(stderr, "failed to connect to Wayland display\n"); + std::exit(EXIT_FAILURE); + } + + auto handle_registry_global = [](void *data, wl_registry *registry, + uint32_t name, const char *interface, + uint32_t version) -> void { + auto *app = static_cast(data); + if (std::strcmp(interface, wl_compositor_interface.name) == 0) { + app->m_wayland.compositor = static_cast( + wl_registry_bind(registry, name, &wl_compositor_interface, 4)); + } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + app->m_wayland.layer_shell = (zwlr_layer_shell_v1 *)wl_registry_bind( + registry, name, &zwlr_layer_shell_v1_interface, + version >= 4 ? 4 : version); + } else if (strcmp(interface, + ext_background_effect_manager_v1_interface.name) == 0) { + app->m_wayland.mgr = (ext_background_effect_manager_v1 *)wl_registry_bind( + registry, name, &ext_background_effect_manager_v1_interface, 1); + } else if (strcmp(interface, "org_kde_kwin_blur_manager") == 0) { + app->m_wayland.kde_blur_mgr = + (org_kde_kwin_blur_manager *)wl_registry_bind( + registry, name, &org_kde_kwin_blur_manager_interface, 1); + } + }; + + static wl_registry_listener const registry_listener{ + .global = handle_registry_global, + .global_remove = [](void *, wl_registry *, uint32_t) {}, + }; + + m_wayland.registry = wl_display_get_registry(m_wayland.display); + wl_registry_add_listener(m_wayland.registry, ®istry_listener, this); + + wl_display_roundtrip(m_wayland.display); + + m_wayland.wl_surface = wl_compositor_create_surface(m_wayland.compositor); + if (m_wayland.mgr && !m_wayland.eff) { + m_wayland.eff = ext_background_effect_manager_v1_get_background_effect( + m_wayland.mgr, m_wayland.wl_surface); + } + if (m_wayland.kde_blur_mgr && !m_wayland.kde_blur) { + m_wayland.kde_blur = org_kde_kwin_blur_manager_create( + m_wayland.kde_blur_mgr, m_wayland.wl_surface); + } + + auto apply_blur_full_surface = [&] { + wl_region *r = wl_compositor_create_region(m_wayland.compositor); + wl_region_add(r, 0, 0, m_win_w, m_win_h); + if (m_wayland.eff) { + ext_background_effect_surface_v1_set_blur_region(m_wayland.eff, r); + } + if (m_wayland.kde_blur) { + org_kde_kwin_blur_set_region(m_wayland.kde_blur, r); + } + wl_region_destroy(r); + }; + + m_wayland.layer_surface = zwlr_layer_shell_v1_get_layer_surface( + m_wayland.layer_shell, m_wayland.wl_surface, /*output*/ nullptr, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "waylight-overlay"); + + zwlr_layer_surface_v1_set_anchor(m_wayland.layer_surface, 0); + + zwlr_layer_surface_v1_set_size(m_wayland.layer_surface, m_win_w, m_win_h); + + zwlr_layer_surface_v1_set_exclusive_zone(m_wayland.layer_surface, 0); + + if (zwlr_layer_shell_v1_get_version(m_wayland.layer_shell) >= 3) + zwlr_layer_surface_v1_set_keyboard_interactivity( + m_wayland.layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); + + auto handle_layer_configure = [](void *data, zwlr_layer_surface_v1 *ls, + uint32_t serial, uint32_t w, + uint32_t h) -> void { + auto *app = static_cast(data); + app->m_win_w = (int)(w ? w : app->m_win_w); + app->m_win_h = (int)(h ? h : app->m_win_h); + + zwlr_layer_surface_v1_ack_configure(ls, serial); + + if (!app->m_gl.wegl) { + app->m_gl.wegl = wl_egl_window_create(app->m_wayland.wl_surface, + app->m_win_w, app->m_win_h); + app->m_gl.esurf = + eglCreateWindowSurface(app->m_gl.edpy, app->m_gl.ecfg, + (EGLNativeWindowType)app->m_gl.wegl, nullptr); + eglMakeCurrent(app->m_gl.edpy, app->m_gl.esurf, app->m_gl.esurf, + app->m_gl.ectx); + eglSwapInterval(app->m_gl.edpy, 1); + } else { + wl_egl_window_resize(app->m_gl.wegl, app->m_win_w, app->m_win_h, 0, 0); + } + + if (app->m_wayland.wl_surface) + wl_surface_commit(app->m_wayland.wl_surface); + }; + + auto handle_layer_closed = [](void *data, zwlr_layer_surface_v1 *) -> void { + static_cast(data)->m_running = false; + }; + + static const zwlr_layer_surface_v1_listener lsl = { + .configure = handle_layer_configure, + .closed = handle_layer_closed, + }; + zwlr_layer_surface_v1_add_listener(m_wayland.layer_surface, &lsl, this); + + apply_blur_full_surface(); + + wl_surface_commit(m_wayland.wl_surface); + wl_display_roundtrip(m_wayland.display); +} + +auto App::set_visible(bool visible) -> void { + if (!m_wayland.layer_surface) + return; + + if (visible) { + zwlr_layer_surface_v1_set_size(m_wayland.layer_surface, m_win_w, m_win_h); + wl_surface_commit(m_wayland.wl_surface); + } else { + wl_surface_attach(m_wayland.wl_surface, nullptr, 0, 0); + wl_surface_commit(m_wayland.wl_surface); + } + wl_display_flush(m_wayland.display); + + m_visible = visible; +} + +auto App::init_egl() -> void { + m_gl.edpy = + eglGetDisplay(reinterpret_cast(m_wayland.display)); + eglInitialize(m_gl.edpy, nullptr, nullptr); + + const EGLint cfgAttribs[]{ + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + EGL_NONE, + }; + EGLint n = 0; + eglChooseConfig(m_gl.edpy, cfgAttribs, &m_gl.ecfg, 1, &n); + + const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE}; + m_gl.ectx = + eglCreateContext(m_gl.edpy, m_gl.ecfg, EGL_NO_CONTEXT, ctxAttribs); + + m_gl.wegl = wl_egl_window_create(m_wayland.wl_surface, m_win_w, m_win_h); + m_gl.esurf = eglCreateWindowSurface( + m_gl.edpy, m_gl.ecfg, reinterpret_cast(m_gl.wegl), + nullptr); + + eglMakeCurrent(m_gl.edpy, m_gl.esurf, m_gl.esurf, m_gl.ectx); + eglSwapInterval(m_gl.edpy, 1); + + InitWindow(m_win_w, m_win_h, ""); +} + +auto App::init_signal() -> void { + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGUSR1); + + if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) != 0) { + std::perror("pthread_sigmask"); + std::exit(EXIT_FAILURE); + } + + m_sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); + if (m_sfd == -1) { + std::perror("signalfd"); + std::exit(EXIT_FAILURE); + } +} + +void App::on_settings_changed(XdpSettings * /*self*/, const char *ns, + const char *key, GVariant * /*value*/, + gpointer data) { + auto *app = static_cast(data); + if (g_strcmp0(ns, "org.freedesktop.appearance") == 0 && + g_strcmp0(key, "color-scheme") == 0) { + guint v = xdp_settings_read_uint(app->m_xdp.settings, + "org.freedesktop.appearance", + "color-scheme", NULL, NULL); + + if (v == 1) + app->m_active_theme = Theme::Dark; + else + app->m_active_theme = Theme::Light; + } +} + +auto App::init_theme_portal() -> void { + m_xdp.portal = xdp_portal_new(); + m_xdp.settings = xdp_portal_get_settings(m_xdp.portal); + + guint v = xdp_settings_read_uint(m_xdp.settings, "org.freedesktop.appearance", + "color-scheme", NULL, NULL); + if (v == 1) + m_active_theme = Theme::Dark; + else + m_active_theme = Theme::Light; + + g_signal_connect(m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), + this); +} + +auto App::render_frame() -> void { + glViewport(0, 0, m_win_w, m_win_h); + + BeginDrawing(); + + ClearBackground(theme().window.background); + + DrawFPS(10, 10); + + EndDrawing(); + + eglSwapBuffers(m_gl.edpy, m_gl.esurf); +} + +auto App::pump_events() -> void { + while (g_main_context_iteration(nullptr, false)) + ; + + wl_display_dispatch_pending(m_wayland.display); + wl_display_flush(m_wayland.display); + + pollfd fds[2]{{wl_display_get_fd(m_wayland.display), POLLIN, 0}, + {m_sfd, POLLIN, 0}}; + + auto prepared = (wl_display_prepare_read(m_wayland.display) == 0); + auto ret = poll(fds, 2, 0); + + // Wayland + if (ret > 0 && (fds[0].revents & POLLIN)) { + if (prepared) { + wl_display_read_events(m_wayland.display); + prepared = false; + } + } else if (prepared) { + wl_display_cancel_read(m_wayland.display); + } + + // Signals + if (ret > 0 && (fds[1].revents & POLLIN)) { + signalfd_siginfo si; + while (read(m_sfd, &si, sizeof(si)) == sizeof(si)) { + if (si.ssi_signo == SIGUSR1) { + set_visible(!visible()); + } + } + } +} + +bool check_or_signal_running() { + const char *lock_path = "/tmp/waylight.lock"; + int fd = open(lock_path, O_CREAT | O_RDWR, 0666); + if (fd == -1) + return false; + + if (flock(fd, LOCK_EX | LOCK_NB) == -1) { + FILE *f = fopen(lock_path, "r"); + if (f) { + pid_t pid; + if (fscanf(f, "%d", &pid) == 1) + kill(pid, SIGUSR1); + fclose(f); + } + close(fd); + return true; + } + + ftruncate(fd, 0); + dprintf(fd, "%d\n", getpid()); + return false; +} + +auto main() -> int { + if (check_or_signal_running()) { + return 0; + } + + App app; + app.run(); +}