Compare commits

...

2 Commits

Author SHA1 Message Date
acf480832a Add todo
Signed-off-by: Slendi <slendi@socopon.com>
2025-10-10 03:46:24 +03:00
7834724e53 IME
Signed-off-by: Slendi <slendi@socopon.com>
2025-10-10 03:45:30 +03:00
7 changed files with 680 additions and 28 deletions

View File

@@ -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
)

View File

@@ -1,10 +1,12 @@
#include "App.hpp"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
@@ -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<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<std::size_t>(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<std::size_t>(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);
@@ -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<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 {
@@ -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<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);
}
};
@@ -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<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 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))

View File

@@ -1,5 +1,6 @@
#pragma once
#include <string>
#include <unordered_set>
#include <vector>
@@ -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 <libportal/settings.h>
#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<ImGui> 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<Theme, ColorScheme> m_themes { make_default_themes() };
Theme m_active_theme { Theme::Light };

View File

@@ -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<int>(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<std::size_t>(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<std::size_t>
{
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<TextInputSurrounding>
{
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<int>(info.caret_byte);
info.anchor = static_cast<int>(info.caret_byte);
return info;
}
auto ImGui::text_input_cursor(std::size_t id) const
-> std::optional<TextInputCursor>
{
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<int>(begin_clamped);
state.preedit_cursor_end = static_cast<int>(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<double>(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<int>(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<int>(options.font_size));
full_metrics = m_text_renderer->measure_text(
*m_font, str_view, static_cast<int>(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<int>(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<int>(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<unsigned long long>(
(submitted ? 1 : 0) | (changed ? 2 : 0)) };
}

View File

@@ -3,6 +3,8 @@
#include <bitset>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <raylib.h>
@@ -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<std::size_t>;
auto text_input_surrounding(
std::size_t id, std::pmr::string const &str) const
-> std::optional<TextInputSurrounding>;
auto text_input_cursor(std::size_t id) const
-> std::optional<TextInputCursor>;
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<std::size_t, TextInputState> m_ti_states;

View File

@@ -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<float>(GetScreenWidth()),
static_cast<float>(GetScreenHeight()),
});
Rectangle const input_rect {
0.0f,
0.0f,
static_cast<float>(GetScreenWidth()),
static_cast<float>(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();

4
todo.txt Normal file
View File

@@ -0,0 +1,4 @@
- [ ] Integrate IME directly into ImGui
- [ ] Implement selection in ImGui::text_edit
- [ ] Implement clipboard copy/paste