☰
MooTool 随手记工具栏实现剖析:笔记颜色、行距倍数与列表前缀的 Compose Desktop 功能对齐
2026/10/16 7:23:52 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

导读

本文以 MooTool 下一代 Compose Desktop 客户端(next-compose)中 F01「随手记(QuickNote)工具栏与文件树」的实证文档为核心,深入剖析三项与 Electron 旧版对齐的编辑能力:笔记颜色(写入 frontmatter 并驱动文件树着色)、行距倍数(作用于 RSyntaxTextArea 行高)、无序/有序列表前缀(覆盖选区整行)。读者将理解这三项功能从设计决策(DIFF-036)、核心领域模型(NoteListEngine、NoteColors、NoteFrontmatter)到存储层(NoteVault.list())与渲染层(EditorBuffer.applyTheme、VaultTreeList)的完整调用链,以及 196/196 桌面测试如何印证其与 Electron 行为的一致性,最后还会看到当前仍未覆盖的测试盲区与已知限制。

一、背景:功能对齐(feature-parity)驱动的 DIFF-036

next-compose是 MooTool 基于 JetBrains Compose Multiplatform / Swing 桥接(SwingPanel+ RSyntaxTextArea)重写的桌面客户端。它并非简单照搬旧 Electron 实现,而是逐项比对功能差异并形成 DIFF 文档。设计文档(DIFF-036,影响范围 F01 随手记工具栏与文件树)明确说明:

原行为(Electron):工具栏有笔记颜色、行距倍数、无序/有序列表。颜色写入 frontmatter 并给文件树图标着色。行距按1.65 * lineSpacing作用到编辑器。列表前缀覆盖选区所在整行,编号从 1 起。

也就是说,对齐的不只是字体和语法高亮,还包括列表动作与文档元数据(颜色、行距)。这类功能必须同时改变编辑器正文、文件树节点或文档 frontmatter,无法用简单开关实现,因此 DIFF-036 独立立项,并沉淀出配套证据目录docs/evidence/2026-09-15-note-toolbar/。

本产品(Compose Desktop)在 results.md 中给出的对齐结论是:

  • NoteListEngine与 Electron 的「选区整行前缀」规则一致;
  • NoteColors七色与文件树着色条件一致;
  • EditorBuffer.applyTheme(lineSpacing=2)使行高至少约加倍;
  • NoteVault.list()会带回 frontmatter 中的 color 字段。

二、笔记颜色:从 frontmatter 到文件树的完整链路

2.1 七色定义与归一化

颜色模型由 NoteColors.kt 实现,swatches定义了七档色板:

标识符ARGB 色值用途
default0xFF8A8A8A默认灰,不参与树着色
coral0xFFD97868珊瑚色
yellow0xFFC99535黄色
green0xFF4E9275绿色
blue0xFF4F83CC蓝色
purple0xFF8A72B5紫色
red0xFFC96761红色

三个核心 API 承担了「写入、读取、着色」三个职责:

  • normalize(id):将任意输入(如"Coral"、"UNKNOWN")归一化为合法的色板键,未知值一律回落为default;
  • argb(id):由归一化键取 ARGB 色值,供 ComposeColor(...)渲染;
  • tintsTree(id):仅当normalize(id) != "default"时才对文件树进行着色。

2.2 颜色写入 frontmatter 并持久化

工具栏的颜色选择由 QuickNoteScreen.kt 驱动。色板按钮点击后执行session.metadata = session.metadata.copy(color = id),随后随笔记保存写入 YAML frontmatter。序列化由 NoteFrontmatter.kt 完成,典型产物形如:

--- title: API ideas style: syntax: text/plain font_name: "" font_size: "14" line_spacing: "1.0" color: coral line_wrap: "1" created_at: 2026-09-15T00:00:00Z modified_at: 2026-09-15T00:00:00Z --- # Hello 正文内容...

serialize()对color使用yamlScalar安全转义,保证任意合法色板键都可往返读写;解析侧parse()通过stringValue(values["color"], "default")缺省回落,健壮性有保障。

2.3 文件树着色条件

文件树渲染位于 VaultTreeList.kt:当节点为文件且NoteColors.tintsTree(entry.color)为真时,行首渲染一个 6dp 圆形色点(Box(Modifier.size(6.dp).clip(CircleShape).background(Color(NoteColors.argb(...))))),同时文件名文本本身也使用该颜色;否则按colors.textBody显示。default色不触发着色,与 Electron 行为对齐。

2.4 「未保存仍按会话色显示」的实现细节

QuickNoteScreen.kt 在向VaultTreeList传递条目时做了就地覆盖:

items = vaultItems.map { item -> if (item.relativePath == session.currentFile) item.copy(color = session.metadata.color) else item }

即当前打开文件即使尚未落盘,其会话元数据session.metadata.color也会立即反映到文件树节点上。

2.5 Vaultlist()带回 frontmatter color

证据文档特别强调Vault list()带回 color。NoteVault.kt 的snapshot()对每个非目录条目执行NoteFrontmatter.parse(...),将parsed?.metadata?.color写入VaultEntry.color,list()再经VaultSearchIndex.filter返回。测试 NoteVaultTest.kt 明确验证:

vault.saveNote(note.relativePath, "# Hello\nneedle", note.metadata.copy(color = "coral")) ... assertEquals("coral", vault.list().first { it.relativePath == saved.relativePath }.color)

同时该测试还验证了created_at、modified_at随 frontmatter 往返、font_size: "15"与line_spacing: "1.0"的序列化格式(注意font_size/line_spacing以字符串形式写入)。

三、行距倍数:1.0–2.0 如何抬高 RSTA 行高

3.1 参数范围与归一化

NoteMetadata.lineSpacing默认1.0。UI 层(QuickNoteScreen.kt)提供档位1.0、1.2、1.4、1.6、1.8、2.0(主工具栏直接展示,溢出菜单moreOpen中重复提供同样的档位)。存储层通过NoteFrontmatter.clampLineSpacing做双重约束:

fun clampLineSpacing(value: Double): Double { val normalized = if (value.isFinite()) value else 1.0 return (normalized.coerceIn(1.0, 2.0) * 10).roundToInt() / 10.0 }

任何非有限值回落为1.0,区间限定[1.0, 2.0],并按 0.1 步长取整,防止恶意或脏数据破坏编辑器布局。

3.2 行距如何作用到编辑器

行距不是 Compose 侧属性,而是经由 EditorHost.kt 的lineSpacing参数下传至 EditorBuffer.kt 的applyTheme(...)。其内部applyLineSpacing的实现值得注意:

private fun applyLineSpacing(spacing: Double) { val multiplier = NoteFrontmatter.clampLineSpacing(spacing) if (multiplier <= 1.0) return // 1.0 保持默认行高 runCatching { val field = RSyntaxTextArea::class.java.getDeclaredField("lineHeight") field.isAccessible = true val base = area.lineHeight val next = round(base * multiplier).toInt().coerceAtLeast(base) field.setInt(area, next) area.revalidate() scrollPane.revalidate() area.repaint() } }

关键点:

  • 1.0 语义:multiplier <= 1.0直接返回,不改动 RSTA 默认行高;
  • 反射写私有字段:RSTA 的lineHeight是私有字段,这里通过反射在RSTextArea计算行高之后按倍数抬高;
  • 异常防护:runCatching包裹反射与重绘,升级 RSTA 后若私有字段改名/失效,功能静默回退到默认行高,不会崩溃。

3.3 测试断言「至少约加倍」

证据文档中的「EditorBuffer.applyTheme(lineSpacing=2)行高至少约加倍」由 EditorThemeTest.kt 的用例直接背书:

buffer.applyTheme(false, "Monospaced", 14, true, lineSpacing = 1.0) val compact = buffer.area.lineHeight buffer.applyTheme(false, "Monospaced", 14, true, lineSpacing = 2.0) assertTrue(buffer.area.lineHeight >= compact * 2 - 1) assertTrue(compact > 0)

同一文件还验证了主题色、语法 token(LITERAL_STRING_DOUBLE_QUOTE、LITERAL_NUMBER_DECIMAL_INT)随applyTheme生效。

3.4 与 Electron 的倍数差异(已知限制)

设计文档明确记录了两端行高基数的差异:

  • Electron 采用1.65 * lineSpacing的直接换算;
  • Compose Desktop 以 RSTA 默认行高为基数,lineSpacing=1.0不改默认值,倍数关系与 Electron一致,但绝对行高基数不同。

因此 UI 观感在绝对值上可能与 Electron 存在细微差异,属于有意的「倍数对齐而非绝对值对齐」。同时由于依赖 RSTA 私有字段,升级 RSTA 版本必须回归复测此用例。

四、列表前缀:NoteListEngine 的整行语义

4.1 核心算法

NoteListEngine.kt 是一个无状态的object,提供prefixSelectedLines(text, start, end, prefix)纯函数。其选区扩展逻辑:

  • 空文档:直接插入-(无序)或1.(有序),选区选中新插入内容;
  • 非空:从caretStart向前找到上一个\n得到行首,从caretEnd向后找到下一个\n得到行尾,即把选区扩展覆盖到其所在的完整首行与末行;
  • 对整段选区按行切分:无序行加-前缀;有序行按index + 1生成1.、2.、3.…(编号从 1 起);
  • 返回新文本与扩展后的选区区间,供 UI 重设选区。
enum class Prefix { Bullet, Numbered } val transformed = selected.split('\n').mapIndexed { index, line -> when (prefix) { Prefix.Bullet -> "- $line" Prefix.Numbered -> "${index + 1}. $line" } }.joinToString("\n")

4.2 进入撤销栈

工具栏按钮(QuickNoteScreen.kt 的quickNote.bulletList/quickNote.numberedList)调用私有函数applyListPrefix:

val result = NoteListEngine.prefixSelectedLines( session.editor.text, area.selectionStart, area.selectionEnd, prefix) quickNoteOnEdt { session.editor.setText(result.text, recordUndo = true) // 关键:进入撤销栈 session.editor.select(result.selectionStart, result.selectionEnd) }

setText(recordUndo = true)走atomicDocument(CompoundEdit+UndoManager,上限 400 步),保证「添加列表前缀」这一操作可被一次撤销/重做;随后select()将选区恢复到扩展后的整行范围,方便用户连续加前缀。

4.3 与 Electron 的逐行一致性测试

NoteListEngineTest.kt 用三个用例锚定「选区所在整行」语义:

输入操作期望输出
"alpha\nbeta\ngamma",选区(6,10)(恰为beta行)Bullet"alpha\n- beta\ngamma"
"one\ntwo",选区(0,7)(覆盖两行)Numbered"1. one\n2. two"
空文本Bullet"- "

其中第二个用例证明:多行选区会被逐行编号(1. one\n2. two),而不是只给首行编号。这印证了「覆盖选区所在整行、编号从 1 起」的设计语义。

五、测试证据与未测盲区

5.1 desktopTest 196/196

证据文档给出的结论是desktopTest196/196(0 skipped)。与之直接相关的测试文件包括:

  • NoteListEngineTest.kt:列表前缀整行语义、NoteColors七色与着色条件(NoteColorsTest);
  • EditorThemeTest.kt:行距倍数抬高 RSTAlineHeight;
  • NoteVaultTest.kt:frontmatter 中color/line_spacing的写入、读取与list()回传;
  • NoteFrontmatterTest.kt:frontmatter 解析与clampLineSpacing归一化。

5.2 明确未测项(复现 / 验收时重点关注)

证据文档诚实列出的未测项,可作为后续验收清单:

  • IME/列编辑手工窗口:中文输入法与列编辑(EditorColumnEditPresentation)在真实窗口下的人工验证;
  • 真实窗口截图:当前证据目录仅含results.md,尚无窗口级截图佐证;
  • 托盘权限对话框:macOS 屏幕录制/辅助功能权限弹窗链路;
  • 六套 CSS 逐选择器皮肤:Compose 主题与旧版六套皮肤按选择器逐一比对的皮肤层验证;
  • 三平台安装/升级/卸载:Windows/macOS/Linux 三端安装包生命周期尚未覆盖。

六、受影响范围与已知限制

设计文档「受影响范围」一节还记录了两个必须向用户说明的边界:

  1. JSON Vault 树无 color 字段:JsonVault的VaultEntry与NoteVault不同,JSON 工具树不读取 frontmatter color,NoteVault.snapshot()中解析 color 的逻辑仅作用于随手记文件(txt/md/json/java/js/ts/py/xml/yaml/yml/sql等NoteFrontmatter.noteExtensions白名单);JSON Vault 节点保持默认样式;
  2. 行距基数差异:如前文 3.4 所述,Compose 端以 RSTA 默认行高为基数、1.0 不改默认值,与 Electron 的1.65em基数不同,但倍数关系一致。

七、结论

DIFF-036 与配套证据文档展示了 MooToolnext-compose一项典型的「领域逻辑可测、渲染层桥接 Swing」功能对齐模式:颜色、行距、列表前缀三种能力都被抽取为无 UI 依赖的领域模块(NoteColors、NoteFrontmatter、NoteListEngine),通过 196/196 的desktopTest在无窗口环境下验证与 Electron 的语义一致,再由EditorBuffer/VaultTreeList等薄渲染层落地。对于开发者而言,若要在升级 RSyntaxTextArea 后回归此功能,请优先重跑 EditorThemeTest.kt 的行距用例,并按 5.2 节的未测清单补齐手工验收。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

相关推荐

上一篇:Lux AltairChart 源码解读:Lux 自动可视化背后的 Vega-Lite 图表基类与渲染管线
下一篇:Apache Maka Runtime Resume 恢复权威拆分账本:从 1346 实验到 PR A/B 不变量驱动的平铺落地

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询