diff --git a/.kilocode/rules/rules.md b/.kilocode/rules/rules.md index bccc838..e966545 100644 --- a/.kilocode/rules/rules.md +++ b/.kilocode/rules/rules.md @@ -33,3 +33,4 @@ > **规则**: 所有头文件引用都是相对于模块的,例如#include "types.h"而不是#include "common/types.h" > **规则**: 容器操作应尽量使用ranges库 > **规则**: 所有代码实现必须使用c++23标准 +> **规则**: 每次添加新接口和功能时,必须考虑封装性和可维护性,避免代码重复 diff --git a/example/test_thread/main.cpp b/example/test_thread/main.cpp index 76e185d..f949d6d 100644 --- a/example/test_thread/main.cpp +++ b/example/test_thread/main.cpp @@ -32,7 +32,7 @@ int main(int argc, char* argv[]) { auto tex_size = app.texture_mgr()->get_texture(texture_id)->size().cast(); // 加载字体 - auto font_result = app.font_mgr()->load_font(R"(C:\Windows\Fonts\SIMYOU.TTF)"); + auto font_result = app.font_mgr()->load_font(R"(C:\Windows\Fonts\msyh.ttc)"); if (!font_result.has_value()) throw std::runtime_error("加载字体失败"); auto font_id = font_result.value(); diff --git a/src/app/application.cpp b/src/app/application.cpp index 78eb360..b3c368d 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -257,10 +257,10 @@ namespace mirage::app { state_store_ = std::make_unique(); // ======================================================================== - // 11. 创建控件上下文(传递文本排版器引用) + // 11. 创建控件上下文(传递文本排版器引用、窗口指针和IME管理器) // ======================================================================== - - widget_context_ = std::make_unique(*state_store_, *text_shaper_); + + widget_context_ = std::make_unique(*state_store_, *text_shaper_, window_, &event_router_.get_ime_manager()); // ======================================================================== // 12. 创建线程协调器(传递文本渲染依赖) diff --git a/src/render/pipeline/text_renderer.cpp b/src/render/pipeline/text_renderer.cpp index 4fac7e8..c76a51d 100644 --- a/src/render/pipeline/text_renderer.cpp +++ b/src/render/pipeline/text_renderer.cpp @@ -393,6 +393,14 @@ void text_renderer::render_impl(vk::CommandBuffer cmd, const text_batch& batch) return; } + // 关键修复:在渲染前检查图集是否需要更新 + // 这确保了在 shape_text() 中动态生成的新字形能被正确上传到GPU + auto& atlas = glyph_cache_.get_atlas(); + if (atlas.is_dirty()) { + std::cout << "[TEXT_RENDERER] Atlas is dirty before render, updating texture" << std::endl; + update_atlas_texture(); + } + // 检查 atlas_view_ 是否有效 if (!atlas_view_) { std::cerr << "[TEXT_RENDERER] ERROR: atlas_view_ is null!" << std::endl; diff --git a/src/widget/widget_context.cpp b/src/widget/widget_context.cpp new file mode 100644 index 0000000..3157ea7 --- /dev/null +++ b/src/widget/widget_context.cpp @@ -0,0 +1,19 @@ +#include "widget_context.h" +#include "window_interface.h" + +namespace mirage { + +std::string widget_context::get_clipboard_text() const { + if (window_) { + return window_->get_clipboard_text(); + } + return {}; +} + +void widget_context::set_clipboard_text(std::string_view text) { + if (window_) { + window_->set_clipboard_text(text); + } +} + +} // namespace mirage \ No newline at end of file diff --git a/src/widget/widget_context.h b/src/widget/widget_context.h index f73662b..cddbb82 100644 --- a/src/widget/widget_context.h +++ b/src/widget/widget_context.h @@ -6,11 +6,15 @@ #include "threading/property.h" #include #include +#include +#include namespace mirage { // 前向声明 using layout_write_context = state::widget_state_store::layout_write_context; +class window_interface; +class ime_manager; namespace render::text { class text_shaper; @@ -27,8 +31,8 @@ namespace render::text { /// @note viewport_cache 使用 property 追踪状态变化 class widget_context { public: - explicit widget_context(state::widget_state_store& store, render::text::text_shaper& shaper) - : store_(store), text_shaper_(shaper) {} + explicit widget_context(state::widget_state_store& store, render::text::text_shaper& shaper, window_interface* window = nullptr, ime_manager* ime = nullptr) + : store_(store), text_shaper_(shaper), window_(window), ime_manager_(ime) {} // ======================================================================== // 状态存储访问 @@ -110,12 +114,48 @@ public: [[nodiscard]] render::text::text_shaper& get_text_shaper() const noexcept { return text_shaper_; } + + // ======================================================================== + // 剪贴板服务 + // ======================================================================== + + /// @brief 获取剪贴板文本 + /// @return 剪贴板中的文本内容(UTF-8编码),如果剪贴板为空或窗口不可用则返回空字符串 + [[nodiscard]] std::string get_clipboard_text() const; + + /// @brief 设置剪贴板文本 + /// @param text 要设置到剪贴板的文本(UTF-8编码) + void set_clipboard_text(std::string_view text); + + /// @brief 检查剪贴板是否可用 + /// @return 如果窗口已设置且剪贴板可用则返回true + [[nodiscard]] bool has_clipboard() const noexcept { + return window_ != nullptr; + } + + // ======================================================================== + // IME 服务 + // ======================================================================== + + /// @brief 获取 IME 管理器 + /// @return IME 管理器指针,如果不可用则返回 nullptr + [[nodiscard]] ime_manager* get_ime_manager() const noexcept { + return ime_manager_; + } + + /// @brief 检查 IME 是否可用 + /// @return 如果 IME 管理器已设置则返回 true + [[nodiscard]] bool has_ime() const noexcept { + return ime_manager_ != nullptr; + } private: state::widget_state_store& store_; ///< 状态存储引用 widget::viewport_cache viewport_cache_; ///< 视口缓存(使用 property 追踪状态) widget::viewport_cache_config viewport_cache_config_; ///< 视口缓存配置 render::text::text_shaper& text_shaper_; ///< 文本塑形器引用 + window_interface* window_ = nullptr; ///< 窗口接口(用于剪贴板访问) + ime_manager* ime_manager_ = nullptr; ///< IME 管理器(用于输入法支持) }; } // namespace mirage \ No newline at end of file diff --git a/src/widget/widget_event/focus_manager.cpp b/src/widget/widget_event/focus_manager.cpp index 707365b..163b69f 100644 --- a/src/widget/widget_event/focus_manager.cpp +++ b/src/widget/widget_event/focus_manager.cpp @@ -215,13 +215,13 @@ void focus_manager::unregister_focusable(uint64_t widget_id) { void focus_manager::notify_focus_change(widget_base* old_target, widget_base* new_target, focus_change_reason reason) { - // TODO: 当 widget_base 支持焦点回调后,调用控件的通知方法 - // if (old_target != nullptr) { - // old_target->on_focus_lost(reason); - // } - // if (new_target != nullptr) { - // new_target->on_focus_gained(reason); - // } + // 调用控件的焦点回调 + if (old_target != nullptr) { + old_target->on_focus_lost(); + } + if (new_target != nullptr) { + new_target->on_focus_gained(); + } // 发布焦点变化事件 focus_changed_event event; diff --git a/src/widget/widgets/text_input/text_input.cpp b/src/widget/widgets/text_input/text_input.cpp index ba5c54b..eeff5cb 100644 --- a/src/widget/widgets/text_input/text_input.cpp +++ b/src/widget/widgets/text_input/text_input.cpp @@ -1,7 +1,12 @@ #include "text_input.h" #include "render_collector.h" +#include "widget_context.h" #include "widget_event/event_types.h" +#include "widget_event/ime_manager.h" #include +#include + +#include "text/text_shaper.h" namespace mirage { // ============================================================================ @@ -133,41 +138,64 @@ namespace mirage { } void text_input::copy() { - // TODO: 实现剪贴板集成 - // if (!has_selection()) return; - // std::string selected = get_selected_text(); - // if (auto* ctx = get_context()) { - // ctx->get_clipboard().set_text(selected); - // } + if (!has_selection()) { + return; + } + + std::string selected = get_selected_text(); + if (auto* ctx = get_context()) { + ctx->set_clipboard_text(selected); + } } void text_input::cut() { - // TODO: 实现剪贴板集成 - // if (!has_selection() || is_read_only()) return; - // copy(); - // model_.delete_selection(); - // if (text_changed_callback_) { - // text_changed_callback_(model_.get_text_utf8()); - // } - // ensure_cursor_visible(); - // mark_render_dirty(); + if (!has_selection() || is_read_only()) { + return; + } + + // 先复制选中的文本到剪贴板 + copy(); + + // 然后删除选中的文本 + model_.delete_selection(); + + if (text_changed_callback_) { + text_changed_callback_(model_.get_text_utf8()); + } + + cursor_.reset_blink(); + ensure_cursor_visible(); + mark_render_dirty(); } void text_input::paste() { - // TODO: 实现剪贴板集成 - // if (is_read_only()) return; - // if (auto* ctx = get_context()) { - // std::string clipboard_text = ctx->get_clipboard().get_text(); - // if (!clipboard_text.empty()) { - // model_.delete_selection(); - // model_.insert_utf8(clipboard_text); - // if (text_changed_callback_) { - // text_changed_callback_(model_.get_text_utf8()); - // } - // ensure_cursor_visible(); - // mark_render_dirty(); - // } - // } + if (is_read_only()) { + return; + } + + auto* ctx = get_context(); + if (!ctx || !ctx->has_clipboard()) { + return; + } + + std::string clipboard_text = ctx->get_clipboard_text(); + if (clipboard_text.empty()) { + return; + } + + // 如果有选中文本,先删除 + model_.delete_selection(); + + // 插入剪贴板文本(model_.insert_utf8 会处理最大长度限制) + model_.insert_utf8(clipboard_text); + + if (text_changed_callback_) { + text_changed_callback_(model_.get_text_utf8()); + } + + cursor_.reset_blink(); + ensure_cursor_visible(); + mark_render_dirty(); } void text_input::clear() { @@ -280,8 +308,8 @@ namespace mirage { // 4. 渲染文本或占位符 vec2f_t text_pos = content_pos - vec2f_t{scroll_offset_, 0.0f}; - if (model_.empty()) { - // 显示占位符 + if (model_.empty() && !preedit_.active) { + // 显示占位符(仅当没有预编辑时) if (!placeholder_.empty()) { collector.submit_text( text_pos, @@ -306,10 +334,134 @@ namespace mirage { z_order++ ); } + + // 5. 渲染预编辑文本(如果有) + if (preedit_.active && !preedit_.text.empty()) { + // 计算预编辑文本位置(在光标位置) + float preedit_start_x = calculate_char_x(model_.get_cursor_position()) - scroll_offset_; + vec2f_t preedit_pos{content_pos.x() + preedit_start_x, content_pos.y()}; + + // 渲染预编辑文本 + collector.submit_text( + preedit_pos, + preedit_.text, + style_.preedit_text, + style_.font_size, + style_.font_id, + z_order++ + ); + + // 计算预编辑文本宽度用于下划线 + float preedit_width = 0.0f; + if (auto* ctx = get_context()) { + // 将 UTF-8 预编辑文本转换为 UTF-32 进行测量 + std::u32string preedit_u32; + for (size_t i = 0; i < preedit_.text.size();) { + char32_t cp = 0; + unsigned char c = static_cast(preedit_.text[i]); + if (c < 0x80) { + cp = c; + i += 1; + } else if ((c & 0xE0) == 0xC0) { + cp = (c & 0x1F) << 6; + if (i + 1 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 1]) & 0x3F); + } + i += 2; + } else if ((c & 0xF0) == 0xE0) { + cp = (c & 0x0F) << 12; + if (i + 1 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 1]) & 0x3F) << 6; + } + if (i + 2 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 2]) & 0x3F); + } + i += 3; + } else if ((c & 0xF8) == 0xF0) { + cp = (c & 0x07) << 18; + if (i + 1 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 1]) & 0x3F) << 12; + } + if (i + 2 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 2]) & 0x3F) << 6; + } + if (i + 3 < preedit_.text.size()) { + cp |= (static_cast(preedit_.text[i + 3]) & 0x3F); + } + i += 4; + } else { + i += 1; // 跳过无效字节 + } + preedit_u32.push_back(cp); + } + + auto metrics = ctx->get_text_shaper().measure_text( + preedit_u32, + style_.font_id, + style_.font_size + ); + preedit_width = metrics.width; + } else { + // 回退估算 + preedit_width = static_cast(preedit_.text.size()) * style_.font_size * 0.6f; + } + + // 渲染预编辑下划线 + vec2f_t underline_pos{ + preedit_pos.x(), + preedit_pos.y() + content_size.y() - style_.preedit_underline_width + }; + vec2f_t underline_size{preedit_width, style_.preedit_underline_width}; + + collector.submit_rect( + underline_pos, + underline_size, + style_.preedit_underline, + {}, + z_order++ + ); + } - // 5. 渲染光标(如果有焦点且可见) + // 6. 渲染光标(如果有焦点且可见,且不在预编辑中或在预编辑文本末尾) if (is_focused_ && cursor_.is_visible() && !is_read_only()) { - float cursor_x = calculate_char_x(model_.get_cursor_position()) - scroll_offset_; + float cursor_x; + if (preedit_.active && !preedit_.text.empty()) { + // 在预编辑状态下,光标显示在预编辑文本内的位置 + float preedit_start_x = calculate_char_x(model_.get_cursor_position()) - scroll_offset_; + // 简单地将光标放在预编辑文本末尾 + // TODO: 使用 preedit_.cursor_pos 计算精确位置 + float preedit_width = 0.0f; + if (auto* ctx = get_context()) { + std::u32string preedit_u32; + for (size_t i = 0; i < preedit_.text.size();) { + char32_t cp = 0; + unsigned char c = static_cast(preedit_.text[i]); + if (c < 0x80) { cp = c; i += 1; } + else if ((c & 0xE0) == 0xC0) { + cp = (c & 0x1F) << 6; + if (i + 1 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 1]) & 0x3F); + i += 2; + } else if ((c & 0xF0) == 0xE0) { + cp = (c & 0x0F) << 12; + if (i + 1 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 1]) & 0x3F) << 6; + if (i + 2 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 2]) & 0x3F); + i += 3; + } else if ((c & 0xF8) == 0xF0) { + cp = (c & 0x07) << 18; + if (i + 1 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 1]) & 0x3F) << 12; + if (i + 2 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 2]) & 0x3F) << 6; + if (i + 3 < preedit_.text.size()) cp |= (static_cast(preedit_.text[i + 3]) & 0x3F); + i += 4; + } else { i += 1; } + preedit_u32.push_back(cp); + } + auto metrics = ctx->get_text_shaper().measure_text(preedit_u32, style_.font_id, style_.font_size); + preedit_width = metrics.width; + } + cursor_x = preedit_start_x + preedit_width; + } else { + cursor_x = calculate_char_x(model_.get_cursor_position()) - scroll_offset_; + } vec2f_t cursor_pos{content_pos.x() + cursor_x, content_pos.y()}; vec2f_t cursor_size{style_.cursor_width, content_size.y()}; @@ -385,12 +537,19 @@ namespace mirage { cursor_.reset_blink(); cursor_.set_blink_enabled(true); - // TODO: 启用 IME - // if (auto* ctx = get_context()) { - // auto& ime = ctx->get_ime_manager(); - // ime.enable(); - // ime.set_focused_target(this); - // } + // 启用 IME + if (auto* ctx = get_context()) { + if (auto* ime = ctx->get_ime_manager()) { + ime->enable(); + ime->set_focused_target(this); + + // 订阅 IME 事件 + subscribe_ime_events(); + + // 更新候选窗口位置 + update_ime_candidate_position(); + } + } // 触发回调 if (focus_changed_callback_) { @@ -408,12 +567,19 @@ namespace mirage { cursor_.set_blink_enabled(false); is_dragging_ = false; - // TODO: 禁用 IME - // if (auto* ctx = get_context()) { - // auto& ime = ctx->get_ime_manager(); - // ime.disable(); - // ime.set_focused_target(nullptr); - // } + // 禁用 IME + if (auto* ctx = get_context()) { + if (auto* ime = ctx->get_ime_manager()) { + // 取消订阅 IME 事件 + unsubscribe_ime_events(); + + ime->disable(); + ime->set_focused_target(nullptr); + } + } + + // 清除预编辑状态 + preedit_.clear(); // 触发回调 if (focus_changed_callback_) { @@ -432,6 +598,18 @@ namespace mirage { return event_result::unhandled(); } + // 在 IME 预编辑状态下,Escape 键取消预编辑 + if (preedit_.active && event.key == key_code::ESCAPE) { + if (auto* ctx = get_context()) { + if (auto* ime = ctx->get_ime_manager()) { + ime->cancel_composition(); + } + } + preedit_.clear(); + mark_render_dirty(); + return event_result::handled(); + } + // 处理快捷键 if (handle_shortcut(event)) { return event_result::handled(); @@ -541,6 +719,11 @@ namespace mirage { return event_result::unhandled(); } + // 在 IME 预编辑状态下,字符输入由 IME 处理 + if (preedit_.active) { + return event_result::handled(); + } + // 忽略控制字符 if (event.codepoint < 0x20) { return event_result::unhandled(); @@ -555,6 +738,7 @@ namespace mirage { cursor_.reset_blink(); ensure_cursor_visible(); + update_ime_candidate_position(); mark_render_dirty(); return event_result::handled(); @@ -574,12 +758,39 @@ namespace mirage { return event_result::unhandled(); } + // 检测双击 + auto now = std::chrono::steady_clock::now(); + auto time_diff = std::chrono::duration_cast( + now - last_click_time_ + ).count(); + + double dx = event.global_x - last_click_x_; + double dy = event.global_y - last_click_y_; + double distance = std::sqrt(dx * dx + dy * dy); + + bool is_double_click = (time_diff < double_click_time_ms_) && + (distance < double_click_distance_); + + // 更新点击记录 + last_click_time_ = now; + last_click_x_ = event.global_x; + last_click_y_ = event.global_y; + // 点击测试,获取字符位置 size_t char_pos = hit_test_position( static_cast(event.global_x), static_cast(event.global_y) ); + if (is_double_click && !model_.empty()) { + // 双击选词 + model_.select_word_at(char_pos); + cursor_.reset_blink(); + is_dragging_ = false; // 双击后不进入拖拽模式 + mark_render_dirty(); + return event_result::handled(); + } + // 如果按下 Shift,扩展选择;否则设置光标 bool shift = has_mod(event.modifiers, key_mod::shift); cursor_.set_position(char_pos, shift); @@ -672,6 +883,8 @@ namespace mirage { // ============================================================================ size_t text_input::hit_test_position(float global_x, float global_y) const { + (void)global_y; // y 坐标在单行输入框中不使用 + auto state_opt = get_layout_state(); if (!state_opt) { return 0; @@ -691,23 +904,41 @@ namespace mirage { return 0; } - // 简化实现:使用平均字符宽度估算 - // TODO: 使用 text_shaper 进行精确的命中测试 - const float avg_char_width = style_.font_size * 0.6f; - size_t text_len = model_.length(); - - // 估算最接近的字符位置 - size_t char_pos = 0; - for (size_t i = 0; i <= text_len; ++i) { - float char_x = calculate_char_x(i); - if (text_x < char_x + avg_char_width * 0.5f) { - char_pos = i; - break; - } - char_pos = i; + // 如果点击位置在文本开始之前 + if (text_x <= 0.0f) { + return 0; } - return std::min(char_pos, text_len); + size_t text_len = model_.length(); + + // 使用二分查找找到最接近的字符位置 + size_t left = 0; + size_t right = text_len; + + while (left < right) { + size_t mid = left + (right - left) / 2; + float mid_x = calculate_char_x(mid); + + if (mid_x < text_x) { + left = mid + 1; + } else { + right = mid; + } + } + + // left 现在指向第一个 x 坐标 >= text_x 的位置 + // 判断是更接近 left 还是 left-1 + if (left > 0) { + float left_x = calculate_char_x(left); + float prev_x = calculate_char_x(left - 1); + + // 选择更接近点击位置的字符边界 + if (text_x - prev_x < left_x - text_x) { + return left - 1; + } + } + + return std::min(left, text_len); } float text_input::calculate_char_x(size_t char_index) const { @@ -715,8 +946,22 @@ namespace mirage { return 0.0f; } - // 简化实现:使用平均字符宽度估算 - // TODO: 使用 text_shaper 测量实际文本宽度 + // 尝试使用 text_shaper 进行精确度量 + if (auto* ctx = get_context()) { + const auto& text = model_.get_text(); + if (!text.empty() && char_index <= text.size()) { + // 测量从开头到 char_index 的子串宽度 + std::u32string_view substring(text.data(), char_index); + auto metrics = ctx->get_text_shaper().measure_text( + substring, + style_.font_id, + style_.font_size + ); + return metrics.width; + } + } + + // 回退到估算算法(当 text_shaper 不可用时) const float avg_char_width = style_.font_size * 0.6f; return static_cast(char_index) * avg_char_width; } @@ -754,4 +999,135 @@ namespace mirage { // 限制滚动范围 scroll_offset_ = std::max(0.0f, scroll_offset_); } + + // ============================================================================ + // IME 事件处理 + // ============================================================================ + + void text_input::handle_ime_preedit_start() { + // 预编辑开始,可以做一些初始化 + cursor_.reset_blink(); + mark_render_dirty(); + } + + void text_input::handle_ime_preedit_update(const ime_preedit_info& preedit) { + preedit_ = preedit; + cursor_.reset_blink(); + ensure_cursor_visible(); + mark_render_dirty(); + } + + void text_input::handle_ime_preedit_end() { + preedit_.clear(); + mark_render_dirty(); + } + + void text_input::handle_ime_commit(const std::string& text) { + if (is_read_only() || text.empty()) { + return; + } + + // 清除预编辑状态 + preedit_.clear(); + + // 插入提交的文本 + model_.insert_utf8(text); + + if (text_changed_callback_) { + text_changed_callback_(model_.get_text_utf8()); + } + + cursor_.reset_blink(); + ensure_cursor_visible(); + update_ime_candidate_position(); + mark_render_dirty(); + } + + void text_input::update_ime_candidate_position() { + if (!is_focused_) { + return; + } + + auto* ctx = get_context(); + if (!ctx) { + return; + } + + auto* ime = ctx->get_ime_manager(); + if (!ime) { + return; + } + + auto state_opt = get_layout_state(); + if (!state_opt) { + return; + } + + const auto& layout = *state_opt; + vec2f_t pos = vec2f_t(layout.global_pos()); + + // 计算光标在屏幕上的位置 + float cursor_x = calculate_char_x(model_.get_cursor_position()) - scroll_offset_; + + // 设置候选窗口位置 + // x: 光标位置 + // y: 输入框底部 + // line_height: 字体大小 + ime->set_candidate_position( + static_cast(pos.x() + style_.padding_horizontal + cursor_x), + static_cast(pos.y() + layout.size().y()), + static_cast(style_.font_size) + ); + } + + void text_input::subscribe_ime_events() { + auto* ctx = get_context(); + if (!ctx) { + return; + } + + auto* ime = ctx->get_ime_manager(); + if (!ime) { + return; + } + + // 订阅预编辑开始事件 + ime_subscriptions_.add( + ime->on_preedit_start(), + ime->on_preedit_start().subscribe([this](const ime_preedit_event& event) { + handle_ime_preedit_start(); + (void)event; + }) + ); + + // 订阅预编辑更新事件 + ime_subscriptions_.add( + ime->on_preedit_update(), + ime->on_preedit_update().subscribe([this](const ime_preedit_event& event) { + handle_ime_preedit_update(event.preedit); + }) + ); + + // 订阅预编辑结束事件 + ime_subscriptions_.add( + ime->on_preedit_end(), + ime->on_preedit_end().subscribe([this](const ime_preedit_event& event) { + handle_ime_preedit_end(); + (void)event; + }) + ); + + // 订阅文本提交事件 + ime_subscriptions_.add( + ime->on_commit(), + ime->on_commit().subscribe([this](const ime_commit_event& event) { + handle_ime_commit(event.text); + }) + ); + } + + void text_input::unsubscribe_ime_events() { + // subscription_list 会在 clear() 时自动取消所有订阅 + ime_subscriptions_.clear(); + } } // namespace mirage diff --git a/src/widget/widgets/text_input/text_input.h b/src/widget/widgets/text_input/text_input.h index 6b98b39..44a5bb8 100644 --- a/src/widget/widgets/text_input/text_input.h +++ b/src/widget/widgets/text_input/text_input.h @@ -4,11 +4,14 @@ #include "text_model.h" #include "cursor_controller.h" #include "widget_event/event_result.h" +#include "widget_event/ime_types.h" +#include "threading/pub_sub.h" #include "types.h" #include #include #include #include +#include namespace mirage { /// @brief 文本输入框样式配置 @@ -67,6 +70,14 @@ namespace mirage { uint32_t font_id{0}; ///< 字体 ID float font_size{14.0f}; ///< 字体大小 + + // ======================================================================== + // IME 预编辑样式 + // ======================================================================== + + color preedit_text{color::from_rgba(255, 255, 128, 255)}; ///< 预编辑文本颜色 + color preedit_underline{color::from_rgba(255, 255, 128, 200)}; ///< 预编辑下划线颜色 + float preedit_underline_width{1.0f}; ///< 预编辑下划线宽度 // ======================================================================== // 预设样式 @@ -383,6 +394,33 @@ namespace mirage { /// @brief 根据当前状态获取边框色 /// @return 边框颜色 [[nodiscard]] color get_border_color() const; + + // ======================================================================== + // IME 相关方法 + // ======================================================================== + + /// @brief 处理 IME 预编辑开始事件 + void handle_ime_preedit_start(); + + /// @brief 处理 IME 预编辑更新事件 + /// @param preedit 预编辑信息 + void handle_ime_preedit_update(const ime_preedit_info& preedit); + + /// @brief 处理 IME 预编辑结束事件 + void handle_ime_preedit_end(); + + /// @brief 处理 IME 文本提交事件 + /// @param text 提交的文本(UTF-8 编码) + void handle_ime_commit(const std::string& text); + + /// @brief 更新 IME 候选窗口位置 + void update_ime_candidate_position(); + + /// @brief 订阅 IME 事件 + void subscribe_ime_events(); + + /// @brief 取消订阅 IME 事件 + void unsubscribe_ime_events(); // ======================================================================== // 成员变量 @@ -414,6 +452,33 @@ namespace mirage { /// 是否只读 bool is_read_only_{false}; + + // ======================================================================== + // 双击检测状态 + // ======================================================================== + + /// 上次鼠标按下时间 + std::chrono::steady_clock::time_point last_click_time_{}; + + /// 上次鼠标按下位置 (x, y) + double last_click_x_{0.0}; + double last_click_y_{0.0}; + + /// 双击时间阈值(毫秒) + static constexpr int64_t double_click_time_ms_{500}; + + /// 双击距离阈值(像素) + static constexpr double double_click_distance_{5.0}; + + // ======================================================================== + // IME 状态 + // ======================================================================== + + /// IME 预编辑信息 + ime_preedit_info preedit_; + + /// IME 事件订阅列表(自动管理生命周期) + threading::subscription_list ime_subscriptions_; // ======================================================================== // 回调函数 diff --git a/src/window/desktop/glfw_window.cpp b/src/window/desktop/glfw_window.cpp index 00ca311..745a3c4 100644 --- a/src/window/desktop/glfw_window.cpp +++ b/src/window/desktop/glfw_window.cpp @@ -126,6 +126,7 @@ namespace mirage { glfwSetWindowIconifyCallback(m_window, window_iconify_callback); glfwSetWindowMaximizeCallback(m_window, window_maximize_callback); glfwSetKeyCallback(m_window, key_callback); + glfwSetCharCallback(m_window, char_callback); glfwSetMouseButtonCallback(m_window, mouse_button_callback); glfwSetCursorPosCallback(m_window, cursor_pos_callback); glfwSetScrollCallback(m_window, scroll_callback); @@ -368,6 +369,22 @@ namespace mirage { glfwSetCursorPos(m_window, x, y); } + // ========== 剪贴板 ========== + + auto glfw_window::get_clipboard_text() const -> std::string { + const char* text = glfwGetClipboardString(m_window); + if (text) { + return std::string(text); + } + return {}; + } + + void glfw_window::set_clipboard_text(std::string_view text) { + // GLFW需要以null结尾的字符串,std::string_view不保证null结尾 + std::string text_str(text); + glfwSetClipboardString(m_window, text_str.c_str()); + } + // ========== 显示器相关 ========== auto glfw_window::get_monitors() const -> monitor_list { @@ -561,6 +578,16 @@ namespace mirage { self->m_event_dispatcher.push_event(e); } + void glfw_window::char_callback(GLFWwindow* window, unsigned int codepoint) { + auto* self = static_cast(glfwGetWindowUserPointer(window)); + + event e(event_type::CHAR_INPUT); + e.char_input.codepoint = codepoint; + e.char_input.mods = key_mod::none; // glfwSetCharCallback 不提供修饰键信息 + + self->m_event_dispatcher.push_event(e); + } + void glfw_window::mouse_button_callback(GLFWwindow* window, int button, int action, int mods) { auto* self = static_cast(glfwGetWindowUserPointer(window)); diff --git a/src/window/desktop/glfw_window.h b/src/window/desktop/glfw_window.h index 3494121..02d94f5 100644 --- a/src/window/desktop/glfw_window.h +++ b/src/window/desktop/glfw_window.h @@ -96,6 +96,10 @@ namespace mirage { auto get_cursor_position() const -> std::pair override; void set_cursor_position(double x, double y) override; + // ========== 剪贴板 ========== + [[nodiscard]] auto get_clipboard_text() const -> std::string override; + void set_clipboard_text(std::string_view text) override; + // ========== 显示器相关 ========== auto get_monitors() const -> monitor_list override; auto get_primary_monitor() const -> monitor_info override; @@ -132,6 +136,7 @@ namespace mirage { static void window_iconify_callback(GLFWwindow* window, int iconified); static void window_maximize_callback(GLFWwindow* window, int maximized); static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods); + static void char_callback(GLFWwindow* window, unsigned int codepoint); static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods); static void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos); static void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); diff --git a/src/window/window_interface.h b/src/window/window_interface.h index 04538ce..02e22c7 100644 --- a/src/window/window_interface.h +++ b/src/window/window_interface.h @@ -192,6 +192,18 @@ namespace mirage { /// @param y 光标的新y坐标(相对于窗口) virtual void set_cursor_position(double x, double y) = 0; + // ========== 剪贴板 ========== + + /// 获取剪贴板文本 + /// + /// @return 剪贴板中的文本内容(UTF-8编码),如果剪贴板为空或不包含文本则返回空字符串 + [[nodiscard]] virtual auto get_clipboard_text() const -> std::string = 0; + + /// 设置剪贴板文本 + /// + /// @param text 要设置到剪贴板的文本(UTF-8编码) + virtual void set_clipboard_text(std::string_view text) = 0; + // ========== 显示器相关 ========== /// 获取可用显示器列表