feat: 添加剪贴板支持和IME管理功能,更新相关接口和实现

This commit is contained in:
2025-12-13 15:49:06 +08:00
parent abcbfd7d0c
commit 6d0db27d0b
12 changed files with 628 additions and 75 deletions

View File

@@ -33,3 +33,4 @@
> **规则**: 所有头文件引用都是相对于模块的,例如#include "types.h"而不是#include "common/types.h"
> **规则**: 容器操作应尽量使用ranges库
> **规则**: 所有代码实现必须使用c++23标准
> **规则**: 每次添加新接口和功能时,必须考虑封装性和可维护性,避免代码重复

View File

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

View File

@@ -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. 创建线程协调器(传递文本渲染依赖)

View File

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

View 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

View File

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

View File

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

View File

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

View File

@@ -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_;
// ========================================================================
// 回调函数

View File

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

View File

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

View File

@@ -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;
// ========== 显示器相关 ==========
/// 获取可用显示器列表