Open dustpg opened 5 years ago
本节是关于高层接口相关实现.
由于脏数组的存在, 文档的(视觉)尺寸是无法准确计算的(除非完全缓存完毕), 所以只有一个估计尺寸:
文档高度:
文档高度自然对应垂直滚动条(水平阅读方向下)需要的数据: 干净区域高度 * 已知行数 / 干净行数
干净区域高度 * 已知行数 / 干净行数
或者: 干净区域高度 + 脏区域行 * 默认行高
干净区域高度 + 脏区域行 * 默认行高
文档宽度:
文档宽度则是对应水平滚动条(水平阅读方向下)需要的数据. 宽度和高度略有区别, 自己的处理方法如下:
估计宽度 = [估计宽度, 当前显示最大宽度].max
估计宽度 = 0
上面实现了几乎所有功能, 但是终端用户并不是程序猿而是一般用户, 需要暴露出GUI编辑控制接口给上级, 这里就列出常用的操作(以Windows作为例子):
其中也有配合ALT键的扩展输入, 看情况可以给予支持. 当然还有很多快捷输入, 例如:
具体实现中, 很多操作分为逻辑和视觉. 例如上、下、HOME、END之类的, 可以根据自身情况实现.
GUI级操作
比如只读文档, GUI基本的操作是无法修改的, 但是低级接口肯定必须能修改(不然都没有文档看). 所以针对上述的GUI操作需要提供一系列GUI基本的函数.
同样, GUI操作函数应该返回一个bool表示是否成功操作. 这个个底层的错误不一样, 是终端用户层次的错误, 需要提示用户, 最简单的就是播放错误音效.
bool
算是基础功能, 一般来说我们可以把选择区两端用锚点(anchor)和插入符(caret)区分, 锚点可能在插入符的前(向后选择)、后(向前选择)以及一样(没有选择). 选择方式一般有:
如果选择区只有一行则是: 开始点-结束点. 多行则是:
值得注意的是: 截图可以看出, 不是最后一行的话会选中换行符号(一行是选不中的), 会略微长一点. 所以可以把除开最后一行加上一个空格的宽度, 或者x字高(估计值, 用字体大小除以2足矣). 不过, 视觉行的末尾就没有(EOL).
所以综合起来, 减少分支后, 过程为:
// 1. 设置第一行末尾位置, 最后一行行首位置 set_end(first, *line0); set_start(last, *line1); // 2. 设置第一行行首位置, 最后一行末尾位置 first.left = cell0->metrics.pos + cm0.offset; last.right = cell1->metrics.pos + cm1.offset; set_height(last, *line1); // 3. 中间行设置上一行末尾(需确认EOL), 这一行行首位置 auto box_itr = &first; std::for_each(line0, line1, [=](const VisualLine& vl) mutable noexcept { set_end(*box_itr, vl); set_height(*box_itr, vl); ++box_itr; set_start(*box_itr, vl); });
可能第一行、最后一行是空的(视觉行末尾开始选择, 矩形宽度为0), 如何处理, 待定.
表中列出了不少方式, 其中如果往文档坐标系上移动的话, 可以用模拟用鼠标点击当前插入符号坐标上部偏移一段具体. 下移动同理.
左右移动则需要注意UTF-16的双字编码字符, 以及特殊的内联对象, 避免插入符移动到非法位置.
按住CTRL的字符簇移动可以实现为: 将字符分为符号与非符号, 插入符移动到前后的区域. 鼠标双击就可以实现为: 分别向前与向后查找, 分别设置为锚点与插入符.
文本输入就简单, 如果存在选择区, 则删除选择区. 然后再在插入符处插入文本:
void on_text(const char text[]){ begin_op(); remove_text(get_selection()); insert_text(caret, text); end_op(); }
这是一般情况, 但是特殊地, 例如代码编辑器: 存在配对符号的, 这种情况则是在两端增加.
这里也可以体现出分低级接口与高级接口的好处: 特殊的功能可以通过组装低级接口实现. 特别地, 使用begin/end_op包裹实现一个撤销栈操作的优点可以大幅度放大!
begin/end_op
void on_text_ex(const char text[]) { begin_op(); s = get_selection(); if (s.begin != s.end && is_ex_text(text)){ insert_text(s.end, ex_text_back(text)); insert_text(s.begin, ex_text_front(text)); } else { remove_text(get_selection()); insert_text(caret, text); } end_op(); }
删除文本的控制键有: 退格键与删除键, 当存在选择区时, 这两个键的功能应当是一致的. 否则一个删除插入符前面的一个逻辑字符, 一个删除插入符后面的.
从右往左的阅读方向文字(为方便可以汉字测试, 毕竟以前是右往左)时, 退格键与删除键如何处理方向, 待定.
这里假定退格键就是'退格'——逻辑左移动
void on_back() { s = get_selection(); begin_op(); // 没有选择区 if (s.begin == s.end) { s.end = caret; s.begin = logic_left_move(caret); } remove_text(s); caret = s.begin; end_op(); } void on_delete() { s = get_selection(); begin_op(); // 没有选择区 if (s.begin == s.end) { s.begin = caret; s.end = logic_right_move(caret); } remove_text(s); end_op(); }
有很多操作处理后, 需要移动视口. 当插入符移动时, 移出视口的情况, 视口应该跟随插入符. 而插入符位置固定, 主动移动视口(鼠标滚轮, CTRL+上下键)则仅仅移动视口.
其中有一个比较特殊的操作: 鼠标选中文本后, 将鼠标指针移动到视口外部. 特殊点在于: 鼠标移出后, 不移动的情况怎么处理.
这里就选择windows自带记事本的处理方法, 操作与结果一对一. 如果外部需要模拟第二种, 也能直接调用函数模拟.
文档需要对数据修改做出反应, 给出消息提示. 比如text_changed消息提示文本修改了.
text_changed
但是给出这些提示是为了方便、即时地获取最新的信息:
void on_text_changed(doc_t& doc) { string_t str; doc.gen_text(str); }
可以看出, 触发text_changed消息时, 必须保证文本可以完整获取. 其次, 没有必要一帧触发多次, 只需要触发最后一次就行.
当然, 几乎没有办法获取自己是不是是最后一次修改, 所以进行延迟处理. 例如要求每帧调用不管渲不渲染, 都要在渲染前调用update()函数用来检测修改信息.
update()
不少文本框只能输入单行, 输入回车会发出一个'确定'的消息. 当然对于多行的文本输入, CTRL+RETRUN也可以发出一个确定的消息.
CTRL+RETRUN
文本输入框自然需要有一个密码模式. 不过, 由于UTF-16的存在, 密码模式很特殊,因为选择的位置和逻辑位置不一定一致.
这里明明只有4个圆圈, 但是有6个UTF-16字符. 选择区间[2, 4)只有一个字.
[2, 4)
解决方法可以有:
char16_t
[2, 3)
方法1自然很简单, 完全不用管; 在考虑密码字符是UCS4字符集的情况, 比如密码字符是emoji'😨': 剩下两个一个是内部一个是外部处理, 最'好'的应该是3(外部、底层). 外部实现时, 可以专门实现一个接口处理密码的情况(甚至内部可以不知道是不是密码模式).
用C++实现富文本控件: 高层
本节是关于高层接口相关实现.
文档尺寸估计
由于脏数组的存在, 文档的(视觉)尺寸是无法准确计算的(除非完全缓存完毕), 所以只有一个估计尺寸:
文档高度:
文档高度自然对应垂直滚动条(水平阅读方向下)需要的数据:
干净区域高度 * 已知行数 / 干净行数
或者:
干净区域高度 + 脏区域行 * 默认行高
文档宽度:
文档宽度则是对应水平滚动条(水平阅读方向下)需要的数据. 宽度和高度略有区别, 自己的处理方法如下:
估计宽度 = [估计宽度, 当前显示最大宽度].max
估计宽度 = 0
编辑控制
上面实现了几乎所有功能, 但是终端用户并不是程序猿而是一般用户, 需要暴露出GUI编辑控制接口给上级, 这里就列出常用的操作(以Windows作为例子):
其中也有配合ALT键的扩展输入, 看情况可以给予支持. 当然还有很多快捷输入, 例如:
具体实现中, 很多操作分为逻辑和视觉. 例如上、下、HOME、END之类的, 可以根据自身情况实现.
GUI级操作
比如只读文档, GUI基本的操作是无法修改的, 但是低级接口肯定必须能修改(不然都没有文档看). 所以针对上述的GUI操作需要提供一系列GUI基本的函数.
同样, GUI操作函数应该返回一个
bool
表示是否成功操作. 这个个底层的错误不一样, 是终端用户层次的错误, 需要提示用户, 最简单的就是播放错误音效.选中区域
算是基础功能, 一般来说我们可以把选择区两端用锚点(anchor)和插入符(caret)区分, 锚点可能在插入符的前(向后选择)、后(向前选择)以及一样(没有选择). 选择方式一般有:
如果选择区只有一行则是: 开始点-结束点. 多行则是:
值得注意的是: 截图可以看出, 不是最后一行的话会选中换行符号(一行是选不中的), 会略微长一点. 所以可以把除开最后一行加上一个空格的宽度, 或者x字高(估计值, 用字体大小除以2足矣). 不过, 视觉行的末尾就没有(EOL).
所以综合起来, 减少分支后, 过程为:
可能第一行、最后一行是空的(视觉行末尾开始选择, 矩形宽度为0), 如何处理, 待定.
插入符移动
表中列出了不少方式, 其中如果往文档坐标系上移动的话, 可以用模拟用鼠标点击当前插入符号坐标上部偏移一段具体. 下移动同理.
左右移动则需要注意UTF-16的双字编码字符, 以及特殊的内联对象, 避免插入符移动到非法位置.
按住CTRL的字符簇移动可以实现为: 将字符分为符号与非符号, 插入符移动到前后的区域. 鼠标双击就可以实现为: 分别向前与向后查找, 分别设置为锚点与插入符.
文本输入
文本输入就简单, 如果存在选择区, 则删除选择区. 然后再在插入符处插入文本:
这是一般情况, 但是特殊地, 例如代码编辑器: 存在配对符号的, 这种情况则是在两端增加.
这里也可以体现出分低级接口与高级接口的好处: 特殊的功能可以通过组装低级接口实现. 特别地, 使用
begin/end_op
包裹实现一个撤销栈操作的优点可以大幅度放大!删除
删除文本的控制键有: 退格键与删除键, 当存在选择区时, 这两个键的功能应当是一致的. 否则一个删除插入符前面的一个逻辑字符, 一个删除插入符后面的.
从右往左的阅读方向文字(为方便可以汉字测试, 毕竟以前是右往左)时, 退格键与删除键如何处理方向, 待定.
这里假定退格键就是'退格'——逻辑左移动
文档视口移动
有很多操作处理后, 需要移动视口. 当插入符移动时, 移出视口的情况, 视口应该跟随插入符. 而插入符位置固定, 主动移动视口(鼠标滚轮, CTRL+上下键)则仅仅移动视口.
其中有一个比较特殊的操作: 鼠标选中文本后, 将鼠标指针移动到视口外部. 特殊点在于: 鼠标移出后, 不移动的情况怎么处理.
这里就选择windows自带记事本的处理方法, 操作与结果一对一. 如果外部需要模拟第二种, 也能直接调用函数模拟.
事件消息
文档需要对数据修改做出反应, 给出消息提示. 比如
text_changed
消息提示文本修改了.但是给出这些提示是为了方便、即时地获取最新的信息:
可以看出, 触发
text_changed
消息时, 必须保证文本可以完整获取. 其次, 没有必要一帧触发多次, 只需要触发最后一次就行.当然, 几乎没有办法获取自己是不是是最后一次修改, 所以进行延迟处理. 例如要求每帧调用不管渲不渲染, 都要在渲染前调用
update()
函数用来检测修改信息.单行模式
不少文本框只能输入单行, 输入回车会发出一个'确定'的消息. 当然对于多行的文本输入,
CTRL+RETRUN
也可以发出一个确定的消息.密码模式
文本输入框自然需要有一个密码模式. 不过, 由于UTF-16的存在, 密码模式很特殊,因为选择的位置和逻辑位置不一定一致.
这里明明只有4个圆圈, 但是有6个UTF-16字符. 选择区间
[2, 4)
只有一个字.解决方法可以有:
char16_t
就能表示[2, 3)
但是返回区间[2, 4)
方法1自然很简单, 完全不用管; 在考虑密码字符是UCS4字符集的情况, 比如密码字符是emoji'😨': 剩下两个一个是内部一个是外部处理, 最'好'的应该是3(外部、底层). 外部实现时, 可以专门实现一个接口处理密码的情况(甚至内部可以不知道是不是密码模式).