diff --git a/src/App.cpp b/src/App.cpp index 6b4d64e..1b1525b 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -191,6 +191,8 @@ auto App::run() -> void while (m_running) { pump_events(); tick(); + m_kbd.typing.clear(); + m_kbd.clear_transients(); std::this_thread::sleep_for(std::chrono::milliseconds(16)); } } @@ -249,19 +251,23 @@ auto App::init_wayland() -> void close(fd); } }; - auto kb_enter { [](void *, wl_keyboard *, u32, wl_surface *, - wl_array *) -> void { } }; + 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, u32, u32 key, + 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) { @@ -308,6 +314,11 @@ auto App::init_wayland() -> void 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_w: case XKB_KEY_W: if (ctrl) { @@ -490,6 +501,99 @@ auto App::init_wayland() -> void 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); + // swap old + 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_utf8 = "text/plain;charset=utf-8"; + char const *mime_plain = "text/plain"; + int fds[2]; + if (pipe(fds) != 0) + return; + wl_data_offer_receive(offer, mime_utf8, fds[1]); + wl_display_flush(app->m_wayland.display); + close(fds[1]); + std::string data_utf8; + char buf[4096]; + for (;;) { + ssize_t n = read(fds[0], buf, sizeof(buf)); + if (n > 0) + data_utf8.append(buf, buf + n); + else + break; + } + close(fds[0]); + + if (data_utf8.empty()) { + if (pipe(fds) != 0) + return; + wl_data_offer_receive( + offer, mime_plain, fds[1]); + wl_display_flush(app->m_wayland.display); + close(fds[1]); + std::string data_plain; + for (;;) { + ssize_t n = read(fds[0], buf, sizeof(buf)); + if (n > 0) + data_plain.append(buf, buf + n); + else + break; + } + close(fds[0]); + app->m_clipboard_cache = std::move(data_plain); + } else { + app->m_clipboard_cache = std::move(data_utf8); + } + }, + }; + wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app); + } } }; @@ -981,3 +1085,57 @@ auto App::pump_events() -> void } } } + +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 *mime, int32_t fd) { + auto *app = static_cast(data); + (void)mime; + size_t off = 0; + while (off < app->m_clipboard_cache.size()) { + ssize_t n = write(fd, app->m_clipboard_cache.data() + off, + app->m_clipboard_cache.size() - off); + if (n <= 0) + break; + off += static_cast(n); + } + close(fd); + }, + .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); +} diff --git a/src/App.hpp b/src/App.hpp index ba6748f..c8dc542 100644 --- a/src/App.hpp +++ b/src/App.hpp @@ -6,6 +6,7 @@ #include #include +#include extern "C" { #include "blur-client-protocol.h" #define namespace namespace_ @@ -50,13 +51,19 @@ private: auto ensure_egl_surface() -> void; auto update_blur_region() -> void; auto process_pending_text_input() -> void; - auto update_text_input_state(std::pmr::string const &text, usize id, - Rectangle field_rect) -> void; + auto update_text_input_state( + std::pmr::string const &text, usize id, Rectangle field_rect) -> void; auto theme() const -> ColorScheme const & { return m_themes[m_active_theme]; } + auto clipboard() const -> std::pmr::string const & + { + return m_clipboard_cache; + } + auto clipboard(std::string_view const &str) -> void; + static void on_settings_changed(XdpSettings * /*self*/, char const *ns, char const *key, GVariant * /*value*/, gpointer data); @@ -75,7 +82,13 @@ private: org_kde_kwin_blur *kde_blur {}; zwp_text_input_manager_v3 *text_input_mgr {}; zwp_text_input_v3 *text_input {}; + wl_data_device_manager *ddm {}; + wl_data_device *ddev {}; + wl_data_offer *curr_offer {}; + wl_data_source *curr_source {}; } m_wayland; + std::pmr::string m_clipboard_cache; + u32 m_last_serial { 0 }; struct { EGLDisplay edpy { EGL_NO_DISPLAY }; diff --git a/src/ImGui.cpp b/src/ImGui.cpp index b56daa7..e023e72 100644 --- a/src/ImGui.cpp +++ b/src/ImGui.cpp @@ -19,67 +19,52 @@ struct CodepointSpan { usize end {}; }; -auto decode_utf8(std::string_view text) -> std::vector +constexpr auto utf8_rune_from_first(char const *s) -> u32 +{ + u8 b0 = static_cast(s[0]); + if (b0 < 0x80) + return b0; + + if ((b0 & 0xE0) == 0xC0) + return ((b0 & 0x1F) << 6) | (static_cast(s[1]) & 0x3F); + + if ((b0 & 0xF0) == 0xE0) + return ((b0 & 0x0F) << 12) | ((static_cast(s[1]) & 0x3F) << 6) + | (static_cast(s[2]) & 0x3F); + + if ((b0 & 0xF8) == 0xF0) + return ((b0 & 0x07) << 18) | ((static_cast(s[1]) & 0x3F) << 12) + | ((static_cast(s[2]) & 0x3F) << 6) + | (static_cast(s[3]) & 0x3F); + + return 0xFFFD; +} + +constexpr auto decode_utf8(std::string_view text) -> std::vector { std::vector spans; usize i = 0; spans.reserve(text.size()); while (i < text.size()) { - u8 const byte = static_cast(text[i]); - usize const start = i; - usize length = 1; - u32 cp = 0xFFFD; + u8 b = static_cast(text[i]); + usize len = 1; - if (byte < 0x80) { - cp = byte; - } else if ((byte & 0xE0) == 0xC0) { - if (i + 1 < text.size()) { - u8 const b1 = static_cast(text[i + 1]); - if ((b1 & 0xC0) == 0x80) { - u32 const t = ((static_cast(byte) & 0x1F) << 6) - | (static_cast(b1) & 0x3F); - if (t >= 0x80) { - cp = t; - length = 2; - } - } - } - } else if ((byte & 0xF0) == 0xE0) { - if (i + 2 < text.size()) { - u8 const b1 = static_cast(text[i + 1]); - u8 const b2 = static_cast(text[i + 2]); - if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80) { - u32 const t = ((static_cast(byte) & 0x0F) << 12) - | ((static_cast(b1) & 0x3F) << 6) - | (static_cast(b2) & 0x3F); - if (t >= 0x800 && (t < 0xD800 || t > 0xDFFF)) { - cp = t; - length = 3; - } - } - } - } else if ((byte & 0xF8) == 0xF0) { - if (i + 3 < text.size()) { - u8 const b1 = static_cast(text[i + 1]); - u8 const b2 = static_cast(text[i + 2]); - u8 const b3 = static_cast(text[i + 3]); - if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80 - && (b3 & 0xC0) == 0x80) { - u32 const t = ((static_cast(byte) & 0x07) << 18) - | ((static_cast(b1) & 0x3F) << 12) - | ((static_cast(b2) & 0x3F) << 6) - | (static_cast(b3) & 0x3F); - if (t >= 0x10000 && t <= 0x10FFFF) { - cp = t; - length = 4; - } - } - } - } + if (b < 0x80) + len = 1; + else if ((b & 0xE0) == 0xC0) + len = 2; + else if ((b & 0xF0) == 0xE0) + len = 3; + else if ((b & 0xF8) == 0xF0) + len = 4; - spans.push_back(CodepointSpan { cp, start, start + length }); - i += length; + if (i + len > text.size()) + len = 1; // avoid overflow + + u32 cp = utf8_rune_from_first(text.data() + i); + spans.push_back({ cp, i, i + len }); + i += len; } return spans; @@ -158,14 +143,25 @@ ImGui::ImGui(std::shared_ptr text_renderer) { } -void ImGui::begin(u32 const rune, bool ctrl, bool shift) +void ImGui::begin(u32 const rune, bool ctrl, bool shift, + std::string_view const clipboard, + std::function clipboard_set) { m_rune = rune; m_ctrl = ctrl; m_shift = shift; + m_clipboard = clipboard; + m_clipboard_set = clipboard_set; } -void ImGui::end() { } +void ImGui::end() +{ + m_rune = false; + m_ctrl = false; + m_shift = false; + m_clipboard = {}; + m_clipboard_set = nullptr; +} void ImGui::set_font(FontHandle font) { m_font = font; } @@ -289,6 +285,15 @@ void ImGui::ime_clear_preedit() state.caret_timer = 0.0; } +size_t utf8_length(std::string_view const &s) +{ + size_t count = 0; + for (unsigned char c : s) + if ((c & 0xC0) != 0x80) // continuation bytes + ++count; + return count; +} + auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, TextInputOptions options) -> std::bitset<2> { @@ -479,6 +484,26 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, submitted = true; } break; + case 'v': + if (m_ctrl && !m_clipboard.empty()) { + if (!options.multiline) { + std::string clipboard_no_newlines; + for (auto const &ch : m_clipboard) { + if (ch == '\n' || ch == '\r') + continue; + clipboard_no_newlines.push_back(ch); + } + str.insert(caret_byte, clipboard_no_newlines); + state.current_rune_idx + += utf8_length(clipboard_no_newlines); + } else { + str.insert(caret_byte, m_clipboard); + state.current_rune_idx += utf8_length(m_clipboard); + } + changed = true; + request_refresh = true; + break; + } default: if (m_rune >= 0x20) { auto encoded { encode_utf8(m_rune) }; diff --git a/src/ImGui.hpp b/src/ImGui.hpp index ba7a852..8a760a2 100644 --- a/src/ImGui.hpp +++ b/src/ImGui.hpp @@ -38,7 +38,9 @@ struct ImGui { ImGui(ImGui &&) = default; auto operator=(ImGui &&) -> ImGui & = default; - void begin(u32 const rune, bool ctrl, bool shift); + void begin(u32 const rune, bool ctrl, bool shift, + std::string_view const clipboard, + std::function clipboard_set); void end(); // Bit 0 -> Submitted @@ -127,6 +129,8 @@ private: u32 m_rune {}; // 1234 <-> hjkl arrow keys bool m_ctrl {}; bool m_shift {}; + std::string_view m_clipboard {}; + std::function m_clipboard_set; std::queue