feat: 添加剪贴板支持和IME管理功能,更新相关接口和实现
This commit is contained in:
@@ -33,3 +33,4 @@
|
||||
> **规则**: 所有头文件引用都是相对于模块的,例如#include "types.h"而不是#include "common/types.h"
|
||||
> **规则**: 容器操作应尽量使用ranges库
|
||||
> **规则**: 所有代码实现必须使用c++23标准
|
||||
> **规则**: 每次添加新接口和功能时,必须考虑封装性和可维护性,避免代码重复
|
||||
|
||||
@@ -32,7 +32,7 @@ int main(int argc, char* argv[]) {
|
||||
auto tex_size = app.texture_mgr()->get_texture(texture_id)->size().cast<float>();
|
||||
|
||||
// 加载字体
|
||||
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();
|
||||
|
||||
@@ -257,10 +257,10 @@ namespace mirage::app {
|
||||
state_store_ = std::make_unique<state::widget_state_store>();
|
||||
|
||||
// ========================================================================
|
||||
// 11. 创建控件上下文(传递文本排版器引用)
|
||||
// 11. 创建控件上下文(传递文本排版器引用、窗口指针和IME管理器)
|
||||
// ========================================================================
|
||||
|
||||
widget_context_ = std::make_unique<widget_context>(*state_store_, *text_shaper_);
|
||||
|
||||
widget_context_ = std::make_unique<widget_context>(*state_store_, *text_shaper_, window_, &event_router_.get_ime_manager());
|
||||
|
||||
// ========================================================================
|
||||
// 12. 创建线程协调器(传递文本渲染依赖)
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
src/widget/widget_context.cpp
Normal file
19
src/widget/widget_context.cpp
Normal file
@@ -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
|
||||
@@ -6,11 +6,15 @@
|
||||
#include "threading/property.h"
|
||||
#include <optional>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
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<T> 追踪状态变化
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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<unsigned char>(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<unsigned char>(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<unsigned char>(preedit_.text[i + 1]) & 0x3F) << 6;
|
||||
}
|
||||
if (i + 2 < preedit_.text.size()) {
|
||||
cp |= (static_cast<unsigned char>(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<unsigned char>(preedit_.text[i + 1]) & 0x3F) << 12;
|
||||
}
|
||||
if (i + 2 < preedit_.text.size()) {
|
||||
cp |= (static_cast<unsigned char>(preedit_.text[i + 2]) & 0x3F) << 6;
|
||||
}
|
||||
if (i + 3 < preedit_.text.size()) {
|
||||
cp |= (static_cast<unsigned char>(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<float>(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<unsigned char>(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<unsigned char>(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<unsigned char>(preedit_.text[i + 1]) & 0x3F) << 6;
|
||||
if (i + 2 < preedit_.text.size()) cp |= (static_cast<unsigned char>(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<unsigned char>(preedit_.text[i + 1]) & 0x3F) << 12;
|
||||
if (i + 2 < preedit_.text.size()) cp |= (static_cast<unsigned char>(preedit_.text[i + 2]) & 0x3F) << 6;
|
||||
if (i + 3 < preedit_.text.size()) cp |= (static_cast<unsigned char>(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<std::chrono::milliseconds>(
|
||||
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<float>(event.global_x),
|
||||
static_cast<float>(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<float>(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<int32_t>(pos.x() + style_.padding_horizontal + cursor_x),
|
||||
static_cast<int32_t>(pos.y() + layout.size().y()),
|
||||
static_cast<int32_t>(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
|
||||
|
||||
@@ -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 <string>
|
||||
#include <string_view>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <chrono>
|
||||
|
||||
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_;
|
||||
|
||||
// ========================================================================
|
||||
// 回调函数
|
||||
|
||||
@@ -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<glfw_window*>(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<glfw_window*>(glfwGetWindowUserPointer(window));
|
||||
|
||||
|
||||
@@ -96,6 +96,10 @@ namespace mirage {
|
||||
auto get_cursor_position() const -> std::pair<double, double> 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
// ========== 显示器相关 ==========
|
||||
|
||||
/// 获取可用显示器列表
|
||||
|
||||
Reference in New Issue
Block a user