Files
waylight/src/App.cpp
2025-10-22 11:27:13 +03:00

1329 lines
37 KiB
C++

#include "App.hpp"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <poll.h>
#include <print>
#include <pthread.h>
#include <ranges>
#include <signal.h>
#include <span>
#include <sys/mman.h>
#include <sys/signalfd.h>
#include <thread>
#include <unistd.h>
#include <unordered_set>
#include <vector>
#include <GLES3/gl3.h>
#include <fontconfig/fontconfig.h>
#include <glib.h>
#include <raylib.h>
#include <rlgl.h>
#include <xkbcommon/xkbcommon.h>
#define namespace namespace_
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
#undef namespace
#include "blur-client-protocol.h"
#include "ext-background-effect-v1-client-protocol.h"
namespace Waylight {
namespace {
constexpr usize MAX_SURROUNDING_BYTES = 4000;
inline auto is_utf8_continuation(char c) -> bool
{
return (static_cast<unsigned char>(c) & 0xC0) == 0x80;
}
inline auto adjust_utf8_backward(std::string const &text, int index) -> int
{
index = std::clamp(index, 0, static_cast<int>(text.size()));
while (
index > 0 && is_utf8_continuation(text[static_cast<usize>(index - 1)]))
--index;
return index;
}
inline auto adjust_utf8_forward(std::string const &text, int index) -> int
{
int const size = static_cast<int>(text.size());
index = std::clamp(index, 0, size);
while (
index < size && is_utf8_continuation(text[static_cast<usize>(index)]))
++index;
return index;
}
struct SurroundingSlice {
std::string text;
int cursor { 0 };
int anchor { 0 };
};
auto clamp_surrounding_text(std::string const &text, int cursor, int anchor)
-> SurroundingSlice
{
int const size = static_cast<int>(text.size());
cursor = std::clamp(cursor, 0, size);
anchor = std::clamp(anchor, 0, size);
if (text.size() <= MAX_SURROUNDING_BYTES) {
return SurroundingSlice { text, cursor, anchor };
}
int window_start = std::max(0,
std::min(cursor, anchor) - static_cast<int>(MAX_SURROUNDING_BYTES / 2));
int window_end = window_start + static_cast<int>(MAX_SURROUNDING_BYTES);
int const max_pos = std::max(cursor, anchor);
if (window_end < max_pos) {
window_end = max_pos;
window_start
= std::max(0, window_end - static_cast<int>(MAX_SURROUNDING_BYTES));
}
if (window_end > size)
window_end = size;
if (window_end - window_start > static_cast<int>(MAX_SURROUNDING_BYTES)) {
window_start = window_end - static_cast<int>(MAX_SURROUNDING_BYTES);
}
window_start = adjust_utf8_backward(text, window_start);
window_end = adjust_utf8_forward(text, window_end);
if (window_end < window_start)
window_end = window_start;
std::string slice(text.begin() + window_start, text.begin() + window_end);
int const new_cursor
= std::clamp(cursor - window_start, 0, static_cast<int>(slice.size()));
int const new_anchor
= std::clamp(anchor - window_start, 0, static_cast<int>(slice.size()));
return SurroundingSlice { std::move(slice), new_cursor, new_anchor };
}
} // namespace
auto TypingBuffer::push_utf8(char const *s) -> void
{
for (unsigned char const *p = reinterpret_cast<unsigned char const *>(s);
*p;) {
u32 cp = 0;
int len = 0;
if (*p < 0x80) {
cp = *p++;
len = 1;
} else if ((*p & 0xE0) == 0xC0) {
cp = *p++ & 0x1F;
len = 2;
} else if ((*p & 0xF0) == 0xE0) {
cp = *p++ & 0x0F;
len = 3;
} else {
cp = *p++ & 0x07;
len = 4;
}
for (int i = 1; i < len; i++)
cp = (cp << 6) | ((*p++) & 0x3F);
push_back(cp);
}
}
App::App()
{
init_wayland();
init_egl();
init_signal();
init_theme_portal();
{
auto const env = getenv("XDG_DATA_HOME");
if (env && *env) {
if (std::filesystem::exists(env)) {
m_data_home_dir = env;
}
}
if (m_data_home_dir.empty()) {
auto const home = getenv("HOME");
assert(home && *home);
m_data_home_dir = std::filesystem::path(home) / ".local" / "share";
std::filesystem::create_directories(m_data_home_dir);
}
m_data_home_dir /= "waylight";
std::filesystem::create_directories(m_data_home_dir);
}
{
auto const env = getenv("XDG_CONFIG_HOME");
if (env && *env) {
if (std::filesystem::exists(env)) {
m_config_home_dir = env;
}
}
if (m_config_home_dir.empty()) {
auto const home = getenv("HOME");
assert(home && *home);
m_config_home_dir = std::filesystem::path(home) / ".config";
std::filesystem::create_directories(m_config_home_dir);
}
m_config_home_dir /= "waylight";
std::filesystem::create_directories(m_config_home_dir);
m_config = Config::load(m_config_home_dir);
}
m_db = std::make_shared<SQLite::Database>(m_data_home_dir / "data.db",
SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
SQLite::Statement(*m_db, R"(
CREATE TABLE IF NOT EXISTS ApplicationCache (
id INTEGER PRIMARY KEY NOT NULL,
type INTEGER NOT NULL,
desktop_entry_path TEXT NOT NULL,
terminal BOOL NOT NULL,
no_display BOOL NOT NULL,
path TEXT,
comment TEXT,
dbus_activatable BOOL NOT NULL
)
)")
.exec();
SQLite::Statement(*m_db, R"(
CREATE TABLE IF NOT EXISTS ApplicationActionCache (
id INTEGER PRIMARY KEY NOT NULL,
id_app INTEGER NOT NULL,
name TEXT NOT NULL,
exec TEXT,
icon TEXT,
FOREIGN KEY (id_app) REFERENCES ApplicationCache(id)
)
)")
.exec();
m_cache.emplace(m_db);
}
App::~App()
{
if (m_sfd != -1)
close(m_sfd);
for (auto &[_, tex] : m_textures)
UnloadTexture(tex);
destroy_layer_surface();
if (m_gl.edpy != EGL_NO_DISPLAY) {
if (m_gl.ectx != EGL_NO_CONTEXT)
eglDestroyContext(m_gl.edpy, m_gl.ectx);
eglTerminate(m_gl.edpy);
}
if (m_kbd.xkb_state_v)
xkb_state_unref(m_kbd.xkb_state_v);
if (m_kbd.xkb_keymap_v)
xkb_keymap_unref(m_kbd.xkb_keymap_v);
if (m_kbd.xkb_ctx_v)
xkb_context_unref(m_kbd.xkb_ctx_v);
if (m_wayland.text_input) {
zwp_text_input_v3_destroy(m_wayland.text_input);
m_wayland.text_input = nullptr;
}
if (m_wayland.text_input_mgr) {
zwp_text_input_manager_v3_destroy(m_wayland.text_input_mgr);
m_wayland.text_input_mgr = nullptr;
}
if (m_wayland.kbd)
wl_keyboard_destroy(m_wayland.kbd);
if (m_wayland.seat)
wl_seat_destroy(m_wayland.seat);
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::run() -> void
{
SetWindowSize(m_win_w, m_win_h);
while (m_running) {
pump_events();
m_ir.color(m_accent_color);
tick();
m_kbd.typing.clear();
m_kbd.clear_transients();
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
}
auto App::set_visible(bool visible) -> void
{
if (visible == m_visible)
return;
if (visible) {
create_layer_surface();
ensure_egl_surface();
} else {
destroy_layer_surface();
}
if (m_wayland.display)
wl_display_flush(m_wayland.display);
}
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);
}
static wl_keyboard_listener keyboard_listener {};
{
auto kb_keymap { [](void *data, wl_keyboard *, u32 format, i32 fd,
u32 size) -> void {
auto *app { static_cast<App *>(data) };
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
close(fd);
return;
}
void *map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) {
close(fd);
return;
}
if (app->m_kbd.xkb_keymap_v)
xkb_keymap_unref(app->m_kbd.xkb_keymap_v);
if (app->m_kbd.xkb_state_v) {
xkb_state_unref(app->m_kbd.xkb_state_v);
app->m_kbd.xkb_state_v = nullptr;
}
app->m_kbd.xkb_keymap_v = xkb_keymap_new_from_string(
app->m_kbd.xkb_ctx_v, static_cast<char const *>(map),
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
app->m_kbd.xkb_state_v = app->m_kbd.xkb_keymap_v
? xkb_state_new(app->m_kbd.xkb_keymap_v)
: nullptr;
munmap(map, size);
close(fd);
} };
auto kb_enter { [](void *data, wl_keyboard *, u32 serial, wl_surface *,
wl_array *) -> void {
static_cast<App *>(data)->m_last_serial = serial;
} };
auto kb_leave
= [](void *data, wl_keyboard *, u32, wl_surface *) -> void {
static_cast<App *>(data)->m_kbd.held.clear();
};
auto kb_key { [](void *data, wl_keyboard *, u32 serial, u32, u32 key,
u32 state) -> void {
auto *app { static_cast<App *>(data) };
if (!app->m_kbd.xkb_state_v)
return;
app->m_last_serial = serial;
xkb_keycode_t kc = key + 8;
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
app->m_kbd.held.insert(key);
xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_DOWN);
xkb_keysym_t sym
= xkb_state_key_get_one_sym(app->m_kbd.xkb_state_v, kc);
app->m_kbd.pressed_syms.insert(sym);
bool ctrl = app->m_kbd.mod_active("Control");
bool alt = app->m_kbd.mod_active("Mod1");
bool meta = app->m_kbd.mod_active("Mod4");
bool handled = false;
switch (sym) {
case XKB_KEY_Left:
app->m_kbd.typing.push_back(1);
handled = true;
break;
case XKB_KEY_Down:
app->m_kbd.typing.push_back(2);
handled = true;
break;
case XKB_KEY_Up:
app->m_kbd.typing.push_back(3);
handled = true;
break;
case XKB_KEY_Right:
app->m_kbd.typing.push_back(4);
handled = true;
break;
case XKB_KEY_BackSpace:
app->m_kbd.typing.push_back(8);
handled = true;
break;
case XKB_KEY_Delete:
case XKB_KEY_KP_Delete:
app->m_kbd.typing.push_back(0x7F);
handled = true;
break;
case XKB_KEY_Return:
case XKB_KEY_KP_Enter:
app->m_kbd.typing.push_back('\n');
handled = true;
break;
case XKB_KEY_v:
case XKB_KEY_V:
app->m_kbd.typing.push_back('v');
handled = true;
break;
case XKB_KEY_c:
case XKB_KEY_C:
app->m_kbd.typing.push_back('c');
handled = true;
break;
case XKB_KEY_w:
case XKB_KEY_W:
if (ctrl) {
app->m_kbd.typing.push_back(8);
handled = true;
}
break;
case XKB_KEY_a:
case XKB_KEY_A:
if (ctrl) {
app->m_kbd.typing.push_back('a');
handled = true;
}
break;
default:
break;
}
if (!handled && !(ctrl || alt || meta)) {
u32 cp = xkb_keysym_to_utf32(sym);
if (cp >= 0x20) {
char buf[8];
int n = xkb_keysym_to_utf8(sym, buf, sizeof(buf));
if (n > 0)
app->m_kbd.typing.push_utf8(buf);
}
}
} else {
xkb_keysym_t sym
= xkb_state_key_get_one_sym(app->m_kbd.xkb_state_v, kc);
app->m_kbd.released_syms.insert(sym);
app->m_kbd.held.erase(key);
xkb_state_update_key(app->m_kbd.xkb_state_v, kc, XKB_KEY_UP);
}
} };
auto kb_mods { [](void *data, wl_keyboard *, u32, u32 depressed,
u32 latched, u32 locked, u32 group) -> void {
auto *app { static_cast<App *>(data) };
if (!app->m_kbd.xkb_state_v)
return;
xkb_state_update_mask(app->m_kbd.xkb_state_v, depressed, latched,
locked, 0, 0, group);
} };
auto kb_repeat_info { [](void *, wl_keyboard *, i32, i32) -> void { } };
keyboard_listener = { kb_keymap, kb_enter, kb_leave, kb_key, kb_mods,
kb_repeat_info };
}
static zwp_text_input_v3_listener text_input_listener {};
{
auto ti_enter =
[](void *data, zwp_text_input_v3 *,
wl_surface *surface) // cppcheck-suppress constParameterPointer
-> void {
auto *app { static_cast<App *>(data) };
bool const focused_surface
= surface && surface == app->m_wayland.surface;
app->m_ime.seat_focus = focused_surface;
app->m_ime.pending = {};
app->m_ime.pending_done = false;
if (!focused_surface) {
app->m_ime.enabled = false;
app->m_ime.last_surrounding.clear();
if (app->m_gui)
app->m_gui->ime_clear_preedit();
} else {
app->m_ime.surrounding_dirty = true;
}
};
auto ti_leave { [](void *data, zwp_text_input_v3 *,
wl_surface *) -> void {
auto *app { static_cast<App *>(data) };
app->m_ime.seat_focus = false;
app->m_ime.enabled = false;
app->m_ime.pending = {};
app->m_ime.pending_done = false;
app->m_ime.last_surrounding.clear();
if (app->m_gui)
app->m_gui->ime_clear_preedit();
} };
auto ti_preedit { [](void *data, zwp_text_input_v3 *, char const *text,
int32_t cursor_begin,
int32_t cursor_end) -> void {
auto *app { static_cast<App *>(data) };
auto &pending { app->m_ime.pending };
pending.has_preedit = true;
pending.preedit_text = text ? text : "";
pending.cursor_begin = cursor_begin;
pending.cursor_end = cursor_end;
} };
auto ti_commit { [](void *data, zwp_text_input_v3 *,
char const *text) -> void {
auto *app { static_cast<App *>(data) };
auto &pending { app->m_ime.pending };
pending.has_commit = true;
pending.commit_text = text ? text : "";
} };
auto ti_delete { [](void *data, zwp_text_input_v3 *, uint32_t before,
uint32_t after) -> void {
auto *app { static_cast<App *>(data) };
auto &pending { app->m_ime.pending };
pending.has_delete = true;
pending.before = before;
pending.after = after;
} };
auto ti_done
= [](void *data, zwp_text_input_v3 *, uint32_t serial) -> void {
auto *app { static_cast<App *>(data) };
app->m_ime.pending_done = true;
app->m_ime.pending_serial = serial;
app->m_ime.surrounding_dirty = true;
};
text_input_listener
= { ti_enter, ti_leave, ti_preedit, ti_commit, ti_delete, ti_done };
}
static auto ensure_text_input { +[](App *app) -> void {
if (!app->m_wayland.text_input_mgr || !app->m_wayland.seat
|| app->m_wayland.text_input)
return;
app->m_wayland.text_input = zwp_text_input_manager_v3_get_text_input(
app->m_wayland.text_input_mgr, app->m_wayland.seat);
if (!app->m_wayland.text_input)
return;
zwp_text_input_v3_add_listener(
app->m_wayland.text_input, &text_input_listener, app);
app->m_ime.supported = true;
app->m_ime.enabled = false;
app->m_ime.last_surrounding.clear();
app->m_ime.sent_serial = 0;
} };
auto handle_registry_global
= [](void *data, wl_registry *registry, u32 name, char const *interface,
u32 version) -> void {
auto *app { static_cast<App *>(data) };
if (std::strcmp(interface, wl_compositor_interface.name) == 0) {
app->m_wayland.compositor = static_cast<wl_compositor *>(
wl_registry_bind(registry, name, &wl_compositor_interface, 4));
} else if (std::strcmp(interface, wl_seat_interface.name) == 0) {
app->m_wayland.seat = static_cast<wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, 9));
static struct wl_seat_listener const seat_listener = {
.capabilities =
[](void *data, struct wl_seat *seat, u32 caps) {
auto *app { static_cast<App *>(data) };
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
app->m_wayland.kbd = wl_seat_get_keyboard(seat);
wl_keyboard_add_listener(
app->m_wayland.kbd, &keyboard_listener, data);
app->m_ime.seat_focus = false;
}
ensure_text_input(app);
},
.name = [](void *, struct wl_seat *, char const *) {},
};
wl_seat_add_listener(app->m_wayland.seat, &seat_listener, data);
} else if (std::strcmp(interface, zwlr_layer_shell_v1_interface.name)
== 0) {
app->m_wayland.layer_shell = static_cast<zwlr_layer_shell_v1 *>(
wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface,
version >= 4 ? 4 : version));
} else if (std::strcmp(interface,
ext_background_effect_manager_v1_interface.name)
== 0) {
app->m_wayland.mgr
= static_cast<ext_background_effect_manager_v1 *>(
wl_registry_bind(registry, name,
&ext_background_effect_manager_v1_interface, 1));
} else if (std::strcmp(interface, "org_kde_kwin_blur_manager") == 0) {
app->m_wayland.kde_blur_mgr
= static_cast<org_kde_kwin_blur_manager *>(wl_registry_bind(
registry, name, &org_kde_kwin_blur_manager_interface, 1));
} else if (std::strcmp(
interface, zwp_text_input_manager_v3_interface.name)
== 0) {
app->m_wayland.text_input_mgr
= static_cast<zwp_text_input_manager_v3 *>(wl_registry_bind(
registry, name, &zwp_text_input_manager_v3_interface, 1));
app->m_ime.supported = true;
ensure_text_input(app);
} else if (std::strcmp(interface, wl_data_device_manager_interface.name)
== 0) {
app->m_wayland.ddm
= static_cast<wl_data_device_manager *>(wl_registry_bind(
registry, name, &wl_data_device_manager_interface,
std::min<uint32_t>(version, 3)));
if (app->m_wayland.ddm && !app->m_wayland.ddev) {
app->m_wayland.ddev = wl_data_device_manager_get_data_device(
app->m_wayland.ddm, app->m_wayland.seat);
static wl_data_device_listener const ddev_l = {
.data_offer =
[](void *data, wl_data_device *, wl_data_offer *offer) {
auto *app = static_cast<App *>(data);
static wl_data_offer_listener const offer_l = {
.offer =
[](void *data, wl_data_offer *,
char const *mime) {
auto *app = static_cast<App *>(data);
(void)app;
(void)mime;
},
#if WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION
.source_actions
= [](void *, wl_data_offer *, uint32_t) {},
.action
= [](void *, wl_data_offer *, uint32_t) {}
#endif
};
wl_data_offer_add_listener(offer, &offer_l, app);
if (app->m_wayland.curr_offer
&& app->m_wayland.curr_offer != offer)
wl_data_offer_destroy(
app->m_wayland.curr_offer);
app->m_wayland.curr_offer = offer;
},
.enter
= [](void *, wl_data_device *, uint32_t, wl_surface *,
wl_fixed_t, wl_fixed_t, wl_data_offer *) {},
.leave = [](void *, wl_data_device *) {},
.motion = [](void *, wl_data_device *, uint32_t, wl_fixed_t,
wl_fixed_t) {},
.drop = [](void *, wl_data_device *) {},
.selection =
[](void *data, wl_data_device *, wl_data_offer *offer) {
auto *app = static_cast<App *>(data);
if (!offer) {
app->m_clipboard_cache.clear();
return;
}
char const *mime = "text/plain;charset=utf-8";
int fds[2];
if (pipe(fds) != 0)
return;
wl_data_offer_receive(offer, mime, fds[1]);
wl_display_flush(app->m_wayland.display);
close(fds[1]);
int rfd = fds[0];
std::thread([app, rfd, offer]() {
std::string data;
char buf[4096];
for (;;) {
ssize_t n = read(rfd, buf, sizeof buf);
if (n > 0) {
data.append(buf, buf + n);
continue;
}
if (n < 0 && errno == EINTR)
continue;
break;
}
close(rfd);
struct Ctx {
App *app;
wl_data_offer *offer;
std::string data;
};
auto *ctx
= new Ctx { app, offer, std::move(data) };
g_main_context_invoke(
nullptr,
+[](gpointer p) -> gboolean {
auto *ctx = static_cast<Ctx *>(p);
if (!ctx->data.empty())
ctx->app->m_clipboard_cache
= std::move(ctx->data);
if (ctx->offer
== ctx->app->m_wayland.curr_offer) {
wl_data_offer_destroy(ctx->offer);
ctx->app->m_wayland.curr_offer
= nullptr;
}
delete ctx;
return G_SOURCE_REMOVE;
},
ctx);
}).detach();
},
};
wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app);
}
}
};
static wl_registry_listener const registry_listener {
.global = handle_registry_global,
.global_remove = [](void *, wl_registry *, u32) { },
};
m_wayland.registry = wl_display_get_registry(m_wayland.display);
wl_registry_add_listener(m_wayland.registry, &registry_listener, this);
m_kbd.xkb_ctx_v = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
wl_display_roundtrip(m_wayland.display);
create_layer_surface();
}
auto App::init_egl() -> void
{
m_gl.edpy = eglGetDisplay(
reinterpret_cast<EGLNativeDisplayType>(m_wayland.display));
eglInitialize(m_gl.edpy, nullptr, nullptr);
EGLint const 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);
EGLint const ctxAttribs[] = { EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE };
m_gl.ectx
= eglCreateContext(m_gl.edpy, m_gl.ecfg, EGL_NO_CONTEXT, ctxAttribs);
ensure_egl_surface();
{
auto const *env = getenv("WAYLIGHT_DEBUG");
if (env && *env) {
SetTraceLogLevel(LOG_DEBUG);
}
}
InitWindow(m_win_w, m_win_h, "");
m_tr = std::make_shared<TextRenderer>();
m_gui = std::make_shared<ImGui>(m_tr);
auto const font { find_font_path() };
assert(font && "Could not find font");
std::vector<std::filesystem::path> fallback_paths;
std::unordered_set<std::string> seen_paths;
auto const primary_path_str { font->string() };
constexpr char const *fallback_candidates[] = {
"Noto Sans CJK JP:style=Regular",
"Noto Sans CJK SC:style=Regular",
"Noto Sans CJK KR:style=Regular",
"Noto Sans CJK TC:style=Regular",
"sans-serif:lang=ja",
"sans-serif:lang=ko",
"sans-serif:lang=zh-cn",
"sans-serif:lang=zh-tw",
"sans-serif:lang=zh-hk",
"Noto Color Emoji:style=Regular",
};
for (auto const *name : fallback_candidates) {
if (auto fallback { find_font_path(name) }) {
auto const path_str { fallback->string() };
if (path_str == primary_path_str)
continue;
if (!seen_paths.emplace(path_str).second)
continue;
fallback_paths.push_back(*fallback);
}
}
if (fallback_paths.empty()) {
TraceLog(LOG_WARNING,
"No fallback fonts found; some glyphs may render as missing");
}
auto const font_handle { m_tr->load_font(
*font, std::span(fallback_paths)) };
assert(font_handle && "Could not load font");
m_font = *font_handle;
m_gui->set_font(m_font);
}
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*/, char const *ns,
char const *key, GVariant * /*value*/, gpointer data)
{
auto *app { static_cast<App *>(data) };
if (g_strcmp0(ns, "org.freedesktop.appearance") == 0) {
if (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;
} else if (g_strcmp0(key, "accent-color") == 0) {
auto val { xdp_settings_read_value(app->m_xdp.settings,
"org.freedesktop.appearance", "accent-color", NULL, NULL) };
if (val) {
gdouble r, g, b;
g_variant_get(val, "(ddd)", &r, &g, &b);
app->m_accent_color.r = static_cast<u8>(r * 255);
app->m_accent_color.g = static_cast<u8>(g * 255);
app->m_accent_color.b = static_cast<u8>(b * 255);
g_variant_unref(val);
}
}
}
}
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;
auto val { xdp_settings_read_value(m_xdp.settings,
"org.freedesktop.appearance", "accent-color", NULL, NULL) };
if (val) {
gdouble r, g, b;
g_variant_get(val, "(ddd)", &r, &g, &b);
m_accent_color.r = static_cast<u8>(r * 255);
m_accent_color.g = static_cast<u8>(g * 255);
m_accent_color.b = static_cast<u8>(b * 255);
g_variant_unref(val);
}
g_signal_connect(
m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this);
}
auto App::create_layer_surface() -> void
{
if (m_wayland.layer_surface)
return;
if (!m_wayland.compositor || !m_wayland.layer_shell)
return;
m_wayland.surface = wl_compositor_create_surface(m_wayland.compositor);
if (m_wayland.mgr) {
m_wayland.eff = ext_background_effect_manager_v1_get_background_effect(
m_wayland.mgr, m_wayland.surface);
}
if (m_wayland.kde_blur_mgr) {
m_wayland.kde_blur = org_kde_kwin_blur_manager_create(
m_wayland.kde_blur_mgr, m_wayland.surface);
}
m_wayland.layer_surface = zwlr_layer_shell_v1_get_layer_surface(
m_wayland.layer_shell, m_wayland.surface, nullptr,
ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "waylight-overlay");
if (!m_wayland.layer_surface) {
if (m_wayland.eff) {
ext_background_effect_surface_v1_destroy(m_wayland.eff);
m_wayland.eff = nullptr;
}
if (m_wayland.kde_blur) {
org_kde_kwin_blur_destroy(m_wayland.kde_blur);
m_wayland.kde_blur = nullptr;
}
if (m_wayland.surface) {
wl_surface_destroy(m_wayland.surface);
m_wayland.surface = nullptr;
}
return;
}
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,
u32 serial, u32 w, u32 h) -> void {
auto *app { static_cast<App *>(data) };
if (w)
app->m_win_w = static_cast<int>(w);
if (h)
app->m_win_h = static_cast<int>(h);
zwlr_layer_surface_v1_ack_configure(ls, serial);
if (app->m_gl.edpy != EGL_NO_DISPLAY) {
if (!app->m_gl.wegl || app->m_gl.esurf == EGL_NO_SURFACE) {
app->ensure_egl_surface();
} else {
wl_egl_window_resize(
app->m_gl.wegl, app->m_win_w, app->m_win_h, 0, 0);
eglMakeCurrent(app->m_gl.edpy, app->m_gl.esurf, app->m_gl.esurf,
app->m_gl.ectx);
}
}
app->update_blur_region();
if (app->m_wayland.surface)
wl_surface_commit(app->m_wayland.surface);
} };
auto handle_layer_closed { [](void *data, zwlr_layer_surface_v1 *) -> void {
static_cast<App *>(data)->m_running = false;
} };
static zwlr_layer_surface_v1_listener const lsl = {
.configure = handle_layer_configure,
.closed = handle_layer_closed,
};
zwlr_layer_surface_v1_add_listener(m_wayland.layer_surface, &lsl, this);
update_blur_region();
if (m_wayland.surface)
wl_surface_commit(m_wayland.surface);
if (m_wayland.display)
wl_display_roundtrip(m_wayland.display);
ensure_egl_surface();
m_visible = true;
}
auto App::destroy_layer_surface() -> void
{
if (m_gl.edpy != EGL_NO_DISPLAY && m_gl.esurf != EGL_NO_SURFACE) {
eglMakeCurrent(
m_gl.edpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroySurface(m_gl.edpy, m_gl.esurf);
m_gl.esurf = EGL_NO_SURFACE;
}
if (m_gl.wegl) {
wl_egl_window_destroy(m_gl.wegl);
m_gl.wegl = nullptr;
}
if (m_wayland.eff) {
ext_background_effect_surface_v1_destroy(m_wayland.eff);
m_wayland.eff = nullptr;
}
if (m_wayland.kde_blur) {
org_kde_kwin_blur_destroy(m_wayland.kde_blur);
m_wayland.kde_blur = nullptr;
}
if (m_wayland.layer_surface) {
zwlr_layer_surface_v1_destroy(m_wayland.layer_surface);
m_wayland.layer_surface = nullptr;
}
if (m_wayland.surface) {
wl_surface_destroy(m_wayland.surface);
m_wayland.surface = nullptr;
}
if (m_wayland.display)
wl_display_flush(m_wayland.display);
m_visible = false;
}
auto App::ensure_egl_surface() -> void
{
if (m_gl.edpy == EGL_NO_DISPLAY || m_gl.ectx == EGL_NO_CONTEXT)
return;
if (!m_wayland.surface)
return;
if (!m_gl.wegl)
m_gl.wegl = wl_egl_window_create(m_wayland.surface, m_win_w, m_win_h);
if (!m_gl.wegl)
return;
if (m_gl.esurf == EGL_NO_SURFACE) {
m_gl.esurf = eglCreateWindowSurface(m_gl.edpy, m_gl.ecfg,
reinterpret_cast<EGLNativeWindowType>(m_gl.wegl), nullptr);
}
if (m_gl.esurf == EGL_NO_SURFACE)
return;
eglMakeCurrent(m_gl.edpy, m_gl.esurf, m_gl.esurf, m_gl.ectx);
eglSwapInterval(m_gl.edpy, 1);
}
auto App::update_blur_region() -> void
{
if (!m_wayland.compositor)
return;
if (!m_wayland.eff && !m_wayland.kde_blur)
return;
wl_region *region = wl_compositor_create_region(m_wayland.compositor);
if (!region)
return;
wl_region_add(region, 0, 0, m_win_w - 50, m_win_h);
if (m_wayland.eff)
ext_background_effect_surface_v1_set_blur_region(m_wayland.eff, region);
if (m_wayland.kde_blur)
org_kde_kwin_blur_set_region(m_wayland.kde_blur, region);
wl_region_destroy(region);
}
auto App::process_pending_text_input() -> void
{
if (!m_ime.pending_done)
return;
if (!m_gui)
return;
if (!m_ime.bound_text)
return;
if (!m_wayland.text_input)
return;
auto focused { m_gui->focused_text_input() };
if (!focused || *focused != m_ime.bound_id) {
m_ime.pending = {};
m_ime.pending_done = false;
return;
}
m_gui->ime_clear_preedit();
if (m_ime.pending.has_delete) {
m_gui->ime_delete_surrounding(
*m_ime.bound_text, m_ime.pending.before, m_ime.pending.after);
}
if (m_ime.pending.has_commit) {
m_gui->ime_commit_text(*m_ime.bound_text, m_ime.pending.commit_text);
}
if (m_ime.pending.has_preedit) {
m_gui->ime_set_preedit(m_ime.pending.preedit_text,
m_ime.pending.cursor_begin, m_ime.pending.cursor_end);
} else {
m_gui->ime_clear_preedit();
}
m_ime.pending = {};
m_ime.pending_done = false;
m_ime.surrounding_dirty = true;
}
auto App::update_text_input_state(
std::pmr::string const &text, usize id, Rectangle field_rect) -> void
{
if (!m_wayland.text_input || !m_ime.supported || !m_gui)
return;
m_ime.bound_rect = field_rect;
auto focused { m_gui->focused_text_input() };
bool const has_focus { focused && (*focused == id) };
bool const should_enable { has_focus && m_ime.seat_focus };
if (!should_enable) {
if (m_ime.enabled) {
zwp_text_input_v3_disable(m_wayland.text_input);
zwp_text_input_v3_commit(m_wayland.text_input);
m_ime.sent_serial++;
m_ime.enabled = false;
m_ime.last_surrounding.clear();
}
return;
}
bool state_dirty = false;
if (!m_ime.enabled) {
zwp_text_input_v3_enable(m_wayland.text_input);
zwp_text_input_v3_set_content_type(m_wayland.text_input,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
m_ime.enabled = true;
m_ime.surrounding_dirty = true;
state_dirty = true;
}
if (auto info { m_gui->text_input_surrounding(id, text) }) {
auto slice
= clamp_surrounding_text(info->text, info->cursor, info->anchor);
if (m_ime.surrounding_dirty || slice.text != m_ime.last_surrounding
|| slice.cursor != m_ime.last_cursor
|| slice.anchor != m_ime.last_anchor) {
zwp_text_input_v3_set_surrounding_text(m_wayland.text_input,
slice.text.c_str(), slice.cursor, slice.anchor);
m_ime.last_surrounding = std::move(slice.text);
m_ime.last_cursor = slice.cursor;
m_ime.last_anchor = slice.anchor;
state_dirty = true;
}
}
if (auto cursor_info { m_gui->text_input_cursor(id) }) {
Rectangle rect = cursor_info->rect;
int32_t const x = static_cast<int32_t>(std::round(rect.x));
int32_t const y = static_cast<int32_t>(std::round(rect.y));
int32_t const width
= std::max(1, static_cast<int32_t>(std::round(rect.width)));
int32_t const height
= std::max(1, static_cast<int32_t>(std::round(rect.height)));
bool const cur_visible = cursor_info->visible;
if (rect.x != m_ime.last_cursor_rect.x
|| rect.y != m_ime.last_cursor_rect.y
|| rect.width != m_ime.last_cursor_rect.width
|| rect.height != m_ime.last_cursor_rect.height
|| cur_visible != m_ime.last_cursor_visible) {
zwp_text_input_v3_set_cursor_rectangle(
m_wayland.text_input, x, y, width, height);
m_ime.last_cursor_rect = rect;
m_ime.last_cursor_visible = cur_visible;
state_dirty = true;
}
}
if (state_dirty) {
zwp_text_input_v3_commit(m_wayland.text_input);
m_ime.sent_serial++;
m_ime.surrounding_dirty = false;
}
}
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) };
if (ret > 0 && (fds[0].revents & POLLIN)) {
if (prepared) {
wl_display_read_events(m_wayland.display);
prepared = false; // cppcheck-suppress unreadVariable
}
} else if (prepared) {
wl_display_cancel_read(m_wayland.display);
}
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());
}
}
}
}
auto App::clipboard(std::string_view const &str) -> void
{
if (!m_wayland.ddm || !m_wayland.ddev || !m_wayland.seat)
return;
if (m_last_serial == 0)
return;
if (m_wayland.curr_source) {
wl_data_source_destroy(m_wayland.curr_source);
m_wayland.curr_source = nullptr;
}
m_wayland.curr_source
= wl_data_device_manager_create_data_source(m_wayland.ddm);
static wl_data_source_listener const src_l = {
.target = [](void *, wl_data_source *, char const *) {},
.send =
[](void *data, wl_data_source *, char const *, int32_t fd) {
auto *app = static_cast<App *>(data);
int wfd = dup(fd);
close(fd);
std::pmr::string payload = app->m_clipboard_cache;
std::thread([wfd, payload = std::move(payload)]() {
size_t off = 0;
while (off < payload.size()) {
ssize_t n = write(wfd, payload.data() + off,
std::min<size_t>(64 * 1024, payload.size() - off));
if (n > 0) {
off += (size_t)n;
continue;
}
if (n < 0 && (errno == EINTR))
continue;
if (n < 0 && (errno == EAGAIN)) {
std::this_thread::sleep_for(
std::chrono::milliseconds(1));
continue;
}
break;
}
close(wfd);
}).detach();
},
.cancelled =
[](void *data, wl_data_source *src) {
auto *app = static_cast<App *>(data);
if (app->m_wayland.curr_source == src)
app->m_wayland.curr_source = nullptr;
wl_data_source_destroy(src);
},
#if WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION
.dnd_drop_performed = [](void *, wl_data_source *) {},
.dnd_finished = [](void *, wl_data_source *) {},
.action = [](void *, wl_data_source *, uint32_t) {}
#endif
};
wl_data_source_add_listener(m_wayland.curr_source, &src_l, this);
wl_data_source_offer(m_wayland.curr_source, "text/plain;charset=utf-8");
wl_data_source_offer(m_wayland.curr_source, "text/plain");
m_clipboard_cache.assign(str.begin(), str.end());
wl_data_device_set_selection(
m_wayland.ddev, m_wayland.curr_source, m_last_serial);
wl_display_flush(m_wayland.display);
}
void App::execute_command(bool terminal, std::string_view const command)
{
constexpr auto resolve_cmdline { [](std::string_view const cmdline,
std::vector<std::string> &out) {
std::ranges::copy(cmdline | std::views::split(' ')
| std::views::transform(
[](auto &&s) { return std::string(s.begin(), s.end()); }),
std::back_inserter(out));
} };
std::vector<std::string> args;
if (terminal) {
resolve_cmdline(m_config.terminal_cmdline, args);
} else {
args.push_back("/bin/sh");
args.push_back("-c");
}
args.push_back(std::string(command));
if (auto const &exe { args.at(0) }; exe.at(0) != '/') {
auto const *path_env { getenv("PATH") };
if (!(path_env && *path_env)) {
path_env = "/bin";
}
auto const path { std::string(path_env) };
for (auto const &dir :
path | std::views::split(':') | std::views::transform([](auto &&s) {
return std::filesystem::path(s.begin(), s.end());
}) | std::views::filter([](auto &&p) {
return std::filesystem::is_directory(p);
})) {
auto const path = dir / exe;
if (std::filesystem::is_regular_file(path)) {
args[0] = path.string();
}
}
}
std::print("Final args: ");
for (auto const &arg : args) {
std::print("{} ", arg);
}
std::println("");
std::vector<char const *> cargs;
std::transform(args.begin(), args.end(), std::back_inserter(cargs),
[](auto &&s) { return s.c_str(); });
cargs.push_back(nullptr);
auto cargsc { const_cast<char *const *>(cargs.data()) };
auto const pid = fork();
if (pid == 0) {
setsid();
execv(args.at(0).c_str(), cargsc);
} else if (pid < 0) {
throw std::runtime_error("Failed to fork process");
}
}
} // namespace Waylight