Article
富文本编辑器开发学习笔记:Carota输入输出-交互与渲染
如果说编辑器模型 是它的灵魂,那么交互 就是它的肢体,而渲染 则是它的穿着打扮。
# 用户交互
用户与编辑器交互主要有三种途径:键盘、鼠标、还有工具条。让用户能灵活地操控编辑器。
-
键盘:包括文字输入和快键操作。
-
鼠标:包括点击、双击、拖拽等操作。
-
工具条:既用于呈现当前的状态,也用于修改选中文本的样式、插入元素等等。
# 键盘
键盘输入首先需要拥有焦点,Carota的实现是使用一个巧妙的方法 来实现键盘输入,通过一个高度为0的textarea 来模拟,不可见 但仍能接收键盘的输入。

高度为0的textarea
查看加载后的HTML节点,能更好理解元素之间关系:
-
**carotaSpacer**:表示实际内容的宽高,用于实现滚动。 -
**canvas**:只设置为可视区域的大小。 -
**carotaTextArea**:设置高度为0,用于隐藏textarea;同时跟随光标移动textarea,这样输入法就能在光标正下方。

Carota通过监听textarea的 keydown 和 input 事件来处理用户输入(注:它没有处理 compositionstart,因此不支持输入法)。

监听keydown事件

监听input事件
每次选择变化时,textarea更新移动到光标位置,并重新获取焦点维持连续的输入。

# 鼠标
鼠标在Carota中主要有三个操作。实现了编辑器的基本的选择和定位功能。
-
**mousedown**:单击修改光标位置; -
**dblclick**:双击选择整个单词; -
**mousemove**:拖拽选择文本区间。

# 工具条
工具条按钮有两个作用:
-
状态显示:反应当前选中文本的样式;
-
样式修改:让用户更改文本样式或者插入特殊元素。

# 更新模型
用户操作最终转换为对模型的修改,可以归纳为四类:插入文字、删除文字、修改样式 和选区变化。
- 插入:将选区替换为新的文本,并更新选区Selection。

- 删除:将选区内容设置为空,并更新选区Selection。

- 修改样式:首先提取选取的
TextRun,然后与新样式合并,最后也是调用 setText 来替换掉选取的内容。不过这里的内容变成了带有样式的TextRun。

这里面涉及到 **splice**、**layout**、**transaction** 数据核心的处理逻辑。
孤立的去看,有些地方就很难理解,例如:在 splice 修改内容时,为啥要获取word,里面prefix、suffix干什么用的,if逻辑判断的作用和意义是啥?
要理解这些,我们需要统一起来看它们,找到它们共同点,才能理解这些别扭的细节实现:它们都是围绕 Word[] 数据结构展开的。
前面的 splice 修改需要配合后面的 layout 和 transaction,所以把选取涉及的 Word 找出来,并把选取属于 Word 找出来再重新组织成 Word。理解这一层 splice 的代码就好理解了。
splice 方法:前面插入文本的代码没有计算样式,到最后才计算插入文本的样式,这是很好的一个实践。要是提前处理,你就会被绕到细节里面去了,例如:跟随前一个字符样式还是后一个字符,在行首、在段首、在图片前后呢?这都是血和泪的教训啊!


在临界点,单词的两端时,对单词的开始结束位置做判断,做到前后样式一样时,合并进一个TextRun,保证数据的最简。
spliceWordsWithRuns 方法:计算生成区间新的 Word[],并记录到历史,用于后面撤销。

transaction方法:事务执行流程都是回调callback串起来的,从后面往前看更清楚:

-
makeTransaction方法内才真正的创建了log事务的实例。 -
然后执行回调,一直到开始事务的
transaction方法。 -
真正的修改逻辑代码在
makeEditCommand中,通过数组splice替换文档中的words,从wordIndex到count内容替换为最新的 newWords。 -
旧的oldWords保存到事务old中,最终以命令Command的形式追加到 undo堆栈 中。(注:都是function回调,反正我被它绕晕了,你就理解后面撤销就是 修改旧的范围为oldWords就可以了。)

# 渲染
更新完成后,如果内容修改了,就要重新layout。由于它没有段落的概念,只能是全局重新布局。算出文档树布局完后,选区变化就会触发 paint重绘,将修改的内容渲染到画布上。

滚动条滚动和选区Selection变化都是会触发paint重绘的。

滚动事件触发绘制

选区变化触发重绘
在画布Canvas绘制文本的地方打个断点,理一下模型的层次结构:

Editor └─ Doc └─ Frame └─ Line └─ PositionedWord └─ Word └─ Box
然后我们逐个看它们的具体代码:

特别注意 PositionedWord 并不是逐字去画 PositionedChar。而是按一个TextRun样式相同的Box 作为一个整体去画,这样性能更好一点。

# 总结
通过几篇文章概况性的浏览了Carota源代码,相比我们日常使用的编辑器,它是的功能比较简陋的:没有分页、图片插入、表格、段落缩进等。所谓麻雀虽小五脏俱全:
-
数据模型:从模型Run的设计,
-
排版引擎:到排版的character字素处理、split单词拆分,
-
渲染引擎:到渲染的wrap文档树,
-
交互设计:到光标屏幕坐标到字符索引之间的转换,最后交互中的键盘、鼠标、工具条事件。
Carota已经囊括了一个编辑器基本的组件。作为学习文本编辑器的入门案例,让我们对编辑器有一个整体的清晰认识,为后面的深入学习和实践打下基础。
Related
Related posts
-
从使用者到创造者:用 AI 构建你的专属 VS Code 工具链
2026-02-27
-
深入解析 Nano Banana:Google 技术博客四篇精华翻译
2025-08-30
-
富文本编辑器开发学习笔记:Carota模型
2025-08-12
-
树莓派 OpenClaw Browser 看不见摸不着?给它配个 VNC 图形环境,踏实安心的Debug
2026-03-09