#include "App.hpp" #include #include #include #include #include #include #include #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" #undef namespace #include "blur-client-protocol.h" #include "ext-background-effect-v1-client-protocol.h" namespace { constexpr usize MAX_SURROUNDING_BYTES = 4000; inline auto is_utf8_continuation(char c) -> bool { return (static_cast(c) & 0xC0) == 0x80; } inline auto adjust_utf8_backward(std::string const &text, int index) -> int { index = std::clamp(index, 0, static_cast(text.size())); while ( index > 0 && is_utf8_continuation(text[static_cast(index - 1)])) --index; return index; } inline auto adjust_utf8_forward(std::string const &text, int index) -> int { int const size = static_cast(text.size()); index = std::clamp(index, 0, size); while ( index < size && is_utf8_continuation(text[static_cast(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(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(MAX_SURROUNDING_BYTES / 2)); int window_end = window_start + static_cast(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(MAX_SURROUNDING_BYTES)); } if (window_end > size) window_end = size; if (window_end - window_start > static_cast(MAX_SURROUNDING_BYTES)) { window_start = window_end - static_cast(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(slice.size())); int const new_anchor = std::clamp(anchor - window_start, 0, static_cast(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(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(); } App::~App() { if (m_sfd != -1) close(m_sfd); 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(); 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(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(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(data)->m_last_serial = serial; } }; auto kb_leave = [](void *data, wl_keyboard *, u32, wl_surface *) -> void { static_cast(data)->m_kbd.held.clear(); }; auto kb_key { [](void *data, wl_keyboard *, u32 serial, u32, u32 key, u32 state) -> void { auto *app { static_cast(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; 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(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) -> void { auto *app { static_cast(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(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(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(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(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(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(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 (std::strcmp(interface, wl_seat_interface.name) == 0) { app->m_wayland.seat = static_cast( 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(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( 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( 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(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(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_registry_bind( registry, name, &wl_data_device_manager_interface, std::min(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(data); static wl_data_offer_listener const offer_l = { .offer = [](void *data, wl_data_offer *, char const *mime) { auto *app = static_cast(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(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(p); if (!ctx->data.empty()) ctx->app->m_clipboard_cache = std::move(ctx->data); wl_data_offer_destroy(ctx->offer); 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, ®istry_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(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(); InitWindow(m_win_w, m_win_h, ""); m_tr = std::make_shared(); m_gui = std::make_shared(m_tr); auto const font { find_font_path() }; assert(font && "Could not find font"); std::vector fallback_paths; std::unordered_set 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(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(r * 255); app->m_accent_color.g = static_cast(g * 255); app->m_accent_color.b = static_cast(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(r * 255); m_accent_color.g = static_cast(g * 255); m_accent_color.b = static_cast(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(data) }; if (w) app->m_win_w = static_cast(w); if (h) app->m_win_h = static_cast(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(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(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(std::round(rect.x)); int32_t const y = static_cast(std::round(rect.y)); int32_t const width = std::max(1, static_cast(std::round(rect.width))); int32_t const height = std::max(1, static_cast(std::round(rect.height))); bool const 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 || 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 = 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; } } 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(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(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(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); }