diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aa3180..736a542 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,17 +93,23 @@ set(WLR_LAYER_SHELL_XML "${WLR_PROTOCOLS_DIR}/unstable/wlr-layer-shell-unstable-v1.xml" ) +set(TEXT_INPUT_XML + "${WAYLAND_PROTOCOLS_DIR}/unstable/text-input/text-input-unstable-v3.xml" +) + set(GEN_C_HEADERS "${GEN_DIR}/xdg-shell-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" + "${GEN_DIR}/text-input-unstable-v3-client-protocol.h" ) set(GEN_C_PRIVATES "${GEN_DIR}/xdg-shell-protocol.c" "${GEN_DIR}/wlr-layer-shell-unstable-v1-protocol.c" "${GEN_DIR}/ext-background-effect-v1-protocol.c" "${GEN_DIR}/blur-protocol.c" + "${GEN_DIR}/text-input-unstable-v3-protocol.c" ) add_custom_command( @@ -116,10 +122,13 @@ add_custom_command( # 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" + # text-input-unstable-v3 + COMMAND "${WAYLAND_SCANNER}" client-header "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-client-protocol.h" + COMMAND "${WAYLAND_SCANNER}" private-code "${TEXT_INPUT_XML}" "${GEN_DIR}/text-input-unstable-v3-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}" + DEPENDS "${XDG_SHELL_XML}" "${WLR_LAYER_SHELL_XML}" "${TEXT_INPUT_XML}" COMMENT "Generating Wayland + wlr-layer-shell client headers and private code" VERBATIM ) diff --git a/src/App.cpp b/src/App.cpp index 3981389..f5c379c 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -1,10 +1,12 @@ #include "App.hpp" +#include #include #include #include #include #include +#include #include #include #include @@ -30,6 +32,84 @@ #include "blur-client-protocol.h" #include "ext-background-effect-v1-client-protocol.h" +namespace { + +constexpr std::size_t 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); @@ -82,6 +162,14 @@ App::~App() 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) @@ -267,6 +355,91 @@ auto App::init_wayland() -> void 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 { @@ -285,7 +458,9 @@ auto App::init_wayland() -> void 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 *) {}, }; @@ -306,6 +481,14 @@ auto App::init_wayland() -> void 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); } }; @@ -619,6 +802,128 @@ auto App::update_blur_region() -> void 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, std::size_t 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)) diff --git a/src/App.hpp b/src/App.hpp index 3353e46..088972f 100644 --- a/src/App.hpp +++ b/src/App.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -9,6 +10,7 @@ extern "C" { #include "blur-client-protocol.h" #define namespace namespace_ #include "ext-background-effect-v1-client-protocol.h" +#include "text-input-unstable-v3-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include #undef namespace @@ -47,6 +49,9 @@ private: auto destroy_layer_surface() -> void; 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, std::size_t id, + Rectangle field_rect) -> void; auto theme() const -> ColorScheme const & { return m_themes[m_active_theme]; @@ -68,6 +73,8 @@ private: ext_background_effect_surface_v1 *eff {}; org_kde_kwin_blur_manager *kde_blur_mgr {}; org_kde_kwin_blur *kde_blur {}; + zwp_text_input_manager_v3 *text_input_mgr {}; + zwp_text_input_v3 *text_input {}; } m_wayland; struct { @@ -144,6 +151,38 @@ private: FontHandle m_font; std::shared_ptr m_gui { nullptr }; + struct { + bool supported { false }; + bool seat_focus { false }; + bool enabled { false }; + bool pending_done { false }; + uint32_t pending_serial { 0 }; + uint32_t sent_serial { 0 }; + + std::pmr::string *bound_text { nullptr }; + std::size_t bound_id { 0 }; + Rectangle bound_rect {}; + + struct { + bool has_preedit { false }; + std::string preedit_text; + int cursor_begin { 0 }; + int cursor_end { 0 }; + bool has_commit { false }; + std::string commit_text; + bool has_delete { false }; + uint32_t before { 0 }; + uint32_t after { 0 }; + } pending; + + std::string last_surrounding; + int last_cursor { 0 }; + int last_anchor { 0 }; + Rectangle last_cursor_rect {}; + bool last_cursor_visible { false }; + bool surrounding_dirty { false }; + } m_ime; + enum_array m_themes { make_default_themes() }; Theme m_active_theme { Theme::Light }; diff --git a/src/ImGui.cpp b/src/ImGui.cpp index 1a24ee5..ef82a0f 100644 --- a/src/ImGui.cpp +++ b/src/ImGui.cpp @@ -111,6 +111,40 @@ auto encode_utf8(u32 cp) -> std::string return std::string(buf, len); } +auto rune_index_for_byte(std::string_view text, std::size_t byte_offset) -> int +{ + auto spans = decode_utf8(text); + int idx = 0; + for (auto const &span : spans) { + if (span.start >= byte_offset) + break; + idx++; + } + if (byte_offset >= text.size()) + idx = static_cast(spans.size()); + return idx; +} + +auto clamp_preedit_index(int value, std::size_t text_size) -> std::size_t +{ + if (value < 0) + return 0; + auto const as_size = static_cast(value); + return std::min(as_size, text_size); +} + +auto slice_bytes(std::string_view text, std::size_t begin, std::size_t end) + -> std::string_view +{ + if (begin > text.size()) + begin = text.size(); + if (end > text.size()) + end = text.size(); + if (end < begin) + end = begin; + return std::string_view(text.data() + begin, end - begin); +} + constexpr float HORIZONTAL_PADDING = 6.0f; constexpr float VERTICAL_PADDING = 4.0f; constexpr float CARET_WIDTH = 2.0f; @@ -135,6 +169,127 @@ void ImGui::end() { } void ImGui::set_font(FontHandle font) { m_font = font; } +auto ImGui::focused_text_input() const -> std::optional +{ + if (m_focused_id == 0) + return std::nullopt; + return m_focused_id; +} + +auto ImGui::text_input_surrounding(std::size_t id, + std::pmr::string const &str) const -> std::optional +{ + auto it = m_ti_states.find(id); + if (it == m_ti_states.end()) + return std::nullopt; + TextInputSurrounding info; + info.text.assign(str.data(), str.size()); + info.caret_byte = std::min(it->second.caret_byte, str.size()); + info.cursor = static_cast(info.caret_byte); + info.anchor = static_cast(info.caret_byte); + return info; +} + +auto ImGui::text_input_cursor(std::size_t id) const + -> std::optional +{ + auto it = m_ti_states.find(id); + if (it == m_ti_states.end()) + return std::nullopt; + TextInputCursor cursor; + cursor.rect = it->second.caret_rect; + cursor.visible + = it->second.caret_visible && !it->second.preedit_cursor_hidden; + return cursor; +} + +void ImGui::ime_commit_text(std::pmr::string &str, std::string_view text) +{ + if (m_focused_id == 0) + return; + auto it = m_ti_states.find(m_focused_id); + if (it == m_ti_states.end()) + return; + auto &state = it->second; + std::size_t insert_pos = std::min(state.caret_byte, str.size()); + if (!text.empty()) + str.insert(insert_pos, text); + state.caret_byte = insert_pos + text.size(); + std::string_view const view(str.data(), str.size()); + state.current_rune_idx = rune_index_for_byte(view, state.caret_byte); + state.caret_timer = 0.0; + state.caret_visible = true; + state.external_change = true; +} + +void ImGui::ime_delete_surrounding( + std::pmr::string &str, std::size_t before, std::size_t after) +{ + if (m_focused_id == 0) + return; + auto it = m_ti_states.find(m_focused_id); + if (it == m_ti_states.end()) + return; + auto &state = it->second; + std::size_t caret_byte = std::min(state.caret_byte, str.size()); + std::size_t start = before > caret_byte ? 0 : caret_byte - before; + std::size_t end = std::min(caret_byte + after, str.size()); + if (end > start) { + str.erase(start, end - start); + state.caret_byte = start; + std::string_view const view(str.data(), str.size()); + state.current_rune_idx = rune_index_for_byte(view, state.caret_byte); + state.caret_timer = 0.0; + state.caret_visible = true; + state.external_change = true; + } +} + +void ImGui::ime_set_preedit(std::string text, int cursor_begin, int cursor_end) +{ + if (m_focused_id == 0) + return; + auto it = m_ti_states.find(m_focused_id); + if (it == m_ti_states.end()) + return; + auto &state = it->second; + state.preedit_text = std::move(text); + state.preedit_cursor_hidden = (cursor_begin == -1 && cursor_end == -1); + std::size_t const size = state.preedit_text.size(); + if (state.preedit_cursor_hidden) { + state.preedit_cursor_begin = 0; + state.preedit_cursor_end = 0; + } else { + auto begin_clamped = clamp_preedit_index(cursor_begin, size); + auto end_clamped = clamp_preedit_index(cursor_end, size); + state.preedit_cursor_begin = static_cast(begin_clamped); + state.preedit_cursor_end = static_cast(end_clamped); + } + state.preedit_active + = !state.preedit_text.empty() || !state.preedit_cursor_hidden; + if (state.preedit_active) { + state.caret_timer = 0.0; + state.caret_visible = !state.preedit_cursor_hidden; + } +} + +void ImGui::ime_clear_preedit() +{ + if (m_focused_id == 0) + return; + auto it = m_ti_states.find(m_focused_id); + if (it == m_ti_states.end()) + return; + auto &state = it->second; + state.preedit_text.clear(); + state.preedit_cursor_begin = 0; + state.preedit_cursor_end = 0; + state.preedit_active = false; + state.preedit_cursor_hidden = false; + state.caret_visible = true; + state.caret_timer = 0.0; +} + auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, TextInputOptions options) -> std::bitset<2> { @@ -351,9 +506,14 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, caret_activity = true; } + state.caret_byte = caret_byte; + double const dt = static_cast(GetFrameTime()); if (m_focused_id == id) { - if (caret_activity) { + if (state.preedit_active && state.preedit_cursor_hidden) { + state.caret_visible = false; + state.caret_timer = 0.0; + } else if (caret_activity) { state.caret_timer = 0.0; state.caret_visible = true; } else { @@ -376,15 +536,62 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, Vector2 prefix_metrics { 0.0f, 0.0f }; Vector2 full_metrics { 0.0f, 0.0f }; + Vector2 preedit_metrics { 0.0f, 0.0f }; + Vector2 caret_preedit_metrics { 0.0f, 0.0f }; + Vector2 selection_prefix_metrics { 0.0f, 0.0f }; + Vector2 selection_metrics { 0.0f, 0.0f }; + std::string display_buffer; + bool const has_preedit = state.preedit_active + && (!state.preedit_text.empty() || !state.preedit_cursor_hidden); + if (m_font.has_value() && m_text_renderer) { + int const font_px = static_cast(options.font_size); std::string_view const prefix_view(str.data(), caret_byte); - prefix_metrics = m_text_renderer->measure_text( - *m_font, prefix_view, static_cast(options.font_size)); - full_metrics = m_text_renderer->measure_text( - *m_font, str_view, static_cast(options.font_size)); + prefix_metrics + = m_text_renderer->measure_text(*m_font, prefix_view, font_px); + + if (has_preedit) { + std::string_view const preedit_view( + state.preedit_text.data(), state.preedit_text.size()); + preedit_metrics + = m_text_renderer->measure_text(*m_font, preedit_view, font_px); + + auto caret_idx = clamp_preedit_index( + state.preedit_cursor_end, preedit_view.size()); + caret_preedit_metrics = m_text_renderer->measure_text( + *m_font, slice_bytes(preedit_view, 0, caret_idx), font_px); + + if (!state.preedit_cursor_hidden + && state.preedit_cursor_begin != state.preedit_cursor_end) { + auto sel_begin + = clamp_preedit_index(std::min(state.preedit_cursor_begin, + state.preedit_cursor_end), + preedit_view.size()); + auto sel_end + = clamp_preedit_index(std::max(state.preedit_cursor_begin, + state.preedit_cursor_end), + preedit_view.size()); + selection_prefix_metrics = m_text_renderer->measure_text( + *m_font, slice_bytes(preedit_view, 0, sel_begin), font_px); + selection_metrics = m_text_renderer->measure_text(*m_font, + slice_bytes(preedit_view, sel_begin, sel_end), font_px); + } + + display_buffer.reserve(str.size() + state.preedit_text.size()); + display_buffer.append(prefix_view); + display_buffer.append(preedit_view); + display_buffer.append(str_view.substr(caret_byte)); + full_metrics = m_text_renderer->measure_text(*m_font, + std::string_view(display_buffer.data(), display_buffer.size()), + font_px); + } else { + full_metrics + = m_text_renderer->measure_text(*m_font, str_view, font_px); + } } - state.cursor_position.x = prefix_metrics.x; + float caret_offset = prefix_metrics.x + caret_preedit_metrics.x; + state.cursor_position.x = caret_offset; float const available_width = std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING); @@ -392,11 +599,11 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, state.scroll_offset.x = 0.0f; } else { float &scroll = state.scroll_offset.x; - float caret_local = state.cursor_position.x - scroll; + float caret_local = caret_offset - scroll; if (caret_local > available_width) { - scroll = state.cursor_position.x - available_width; + scroll = caret_offset - available_width; } else if (caret_local < 0.0f) { - scroll = state.cursor_position.x; + scroll = caret_offset; } scroll = std::clamp( scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width)); @@ -431,26 +638,68 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, } state.cursor_position.y = caret_top; + float const caret_draw_x + = rec.x + HORIZONTAL_PADDING + caret_offset - state.scroll_offset.x; + state.caret_rect = Rectangle { + caret_draw_x, + caret_top, + CARET_WIDTH, + caret_height, + }; + BeginScissorMode(rec.x, rec.y, rec.width, rec.height); { if (m_font.has_value() && m_text_renderer) { - Vector2 const text_pos { + int const font_px = static_cast(options.font_size); + Vector2 const base_pos { rec.x + HORIZONTAL_PADDING - state.scroll_offset.x, baseline_y, }; Color const text_color { 255, 255, 255, 255 }; - m_text_renderer->draw_text(*m_font, str_view, text_pos, - static_cast(options.font_size), text_color); + std::string_view const left_view(str.data(), caret_byte); + std::string_view const right_view( + str.data() + caret_byte, str.size() - caret_byte); + + m_text_renderer->draw_text( + *m_font, left_view, base_pos, font_px, text_color); + + float advance = prefix_metrics.x; + if (has_preedit) { + Vector2 const preedit_pos { + base_pos.x + advance, + base_pos.y, + }; + if (selection_metrics.x > 0.0f) { + float const sel_offset + = prefix_metrics.x + selection_prefix_metrics.x; + Rectangle const sel_rect { + rec.x + HORIZONTAL_PADDING + sel_offset + - state.scroll_offset.x, + caret_top, + selection_metrics.x, + caret_height, + }; + Color const highlight { 120, 160, 255, 100 }; + DrawRectangleRec(sel_rect, highlight); + } + Color const preedit_color { 200, 220, 255, 255 }; + m_text_renderer->draw_text(*m_font, + std::string_view( + state.preedit_text.data(), state.preedit_text.size()), + preedit_pos, font_px, preedit_color); + advance += preedit_metrics.x; + } + + Vector2 const right_pos { + base_pos.x + advance, + base_pos.y, + }; + m_text_renderer->draw_text( + *m_font, right_view, right_pos, font_px, text_color); if (m_focused_id == id && state.caret_visible) { - float const caret_x = std::round(rec.x + HORIZONTAL_PADDING - + state.cursor_position.x - state.scroll_offset.x); - Rectangle caret_rect { - caret_x, - caret_top, - CARET_WIDTH, - caret_height, - }; + Rectangle caret_rect = state.caret_rect; + caret_rect.x = std::round(caret_rect.x); Color const caret_color { 255, 255, 255, 200 }; DrawRectangleRec(caret_rect, caret_color); } @@ -458,6 +707,11 @@ auto ImGui::text_input(std::size_t id, std::pmr::string &str, Rectangle rec, } EndScissorMode(); + if (state.external_change) { + changed = true; + state.external_change = false; + } + return std::bitset<2> { static_cast( (submitted ? 1 : 0) | (changed ? 2 : 0)) }; } diff --git a/src/ImGui.hpp b/src/ImGui.hpp index ba62326..70e3cee 100644 --- a/src/ImGui.hpp +++ b/src/ImGui.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include @@ -31,6 +33,30 @@ struct ImGui { auto text_input(std::size_t id, std::pmr::string &str, Rectangle rec, TextInputOptions options = {}) -> std::bitset<2>; + struct TextInputSurrounding { + std::string text; + int cursor { 0 }; + int anchor { 0 }; + std::size_t caret_byte { 0 }; + }; + + struct TextInputCursor { + Rectangle rect {}; + bool visible { true }; + }; + + auto focused_text_input() const -> std::optional; + auto text_input_surrounding( + std::size_t id, std::pmr::string const &str) const + -> std::optional; + auto text_input_cursor(std::size_t id) const + -> std::optional; + void ime_commit_text(std::pmr::string &str, std::string_view text); + void ime_delete_surrounding( + std::pmr::string &str, std::size_t before, std::size_t after); + void ime_set_preedit(std::string text, int cursor_begin, int cursor_end); + void ime_clear_preedit(); + void set_font(FontHandle font); [[nodiscard]] inline auto id(std::string_view const str) -> std::size_t @@ -46,6 +72,14 @@ private: Vector2 cursor_position; // y not used if multiline == false bool caret_visible { true }; double caret_timer { 0.0 }; + std::string preedit_text; + int preedit_cursor_begin { 0 }; + int preedit_cursor_end { 0 }; + bool preedit_active { false }; + bool preedit_cursor_hidden { false }; + std::size_t caret_byte { 0 }; + Rectangle caret_rect {}; + bool external_change { false }; }; std::unordered_map m_ti_states; diff --git a/src/Tick.cpp b/src/Tick.cpp index 78e6b15..91dd3d6 100644 --- a/src/Tick.cpp +++ b/src/Tick.cpp @@ -11,6 +11,9 @@ auto App::tick() -> void { static std::pmr::string text_input_data; + m_ime.bound_text = &text_input_data; + m_ime.bound_id = 1; + process_pending_text_input(); if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY || m_gl.esurf == EGL_NO_SURFACE) @@ -38,13 +41,17 @@ auto App::tick() -> void } ImGuiGuard gui_scope(m_gui, rune, m_kbd.ctrl(), m_kbd.shift()); - m_gui->text_input(1, text_input_data, - { - 0, - 0, - static_cast(GetScreenWidth()), - static_cast(GetScreenHeight()), - }); + Rectangle const input_rect { + 0.0f, + 0.0f, + static_cast(GetScreenWidth()), + static_cast(GetScreenHeight()), + }; + auto result = m_gui->text_input(1, text_input_data, input_rect); + if (result.test(1)) + m_ime.surrounding_dirty = true; + + update_text_input_state(text_input_data, 1, input_rect); } EndDrawing();