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();
+}