MooTool Next Compose 紧凑布局与编辑器字体体系:DIFF-035 设计决策与实践解读 桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载导读MooTool Next Compose 是 MooTool 开发工具集的 Compose Desktop 重构版本。DIFF-035 是该版本在 2026-09-15 落地的一组关键差异化改动集中解决三件事JSON 工具栏的系统字体选择与持久化、窗口内容宽 960dp 以下的「按需辅助栏」布局策略、以及搜索/设置快捷键冲突时的可见提示。本文以 DIFF-035 设计文档为主干结合 JsonScreen.kt、QuickNoteScreen.kt、LayoutPolicy.kt 等源码与desktopTest测试逐层还原实现原理帮助开发者理解该产品在紧凑窗口、字体持久化与快捷键冲突处理上的取舍与验收边界。一、DIFF-035 的背景与影响范围1.1 改动编号与影响面项内容编号DIFF-035日期2026-09-15影响模块F04 JSON 工具栏/窄窗、F01 随手记工具栏/窄窗、A01 shortcuts/editor 字体按 DIFF 编号体系035 是在 DIFF-034系统托盘闭环、软换行默认、960 折叠策略之后对 JSON 工具栏、随手记工具栏与快捷键设置三处行为的精细化收口。它服务于两条产品规格JSON 工具栏必须保留「格式化、压缩、字体、换行」能力ui-spec 的 960 档要求文件树/检查器按需切换而非挤掉编辑器且快捷键冲突必须可见、不能静默丢输入。1.2 原行为Electron 基线Electron 版本中JsonToolbar.tsx在压缩布局下仍提供FontSelect组件并写入json.font当内容宽落在 960–1079 区间时导航折叠文件树/检查器按需切换编辑区占满剩余空间。随手记工具栏提供字体、字号、语法三项控制。设置页在搜索/设置快捷键冲突时会拒绝覆盖并给出原因。1.3 本产品行为概要JSON 工具栏新增系统字体选择写入settings.editor.jsonFontName设置页使用同一个FontSelect组件字体列表为「Electron 回退字体 GraphicsEnvironment已安装字体」。内容宽 960 时 JSON 默认隐藏 Vault 与检查器工具栏「JSON Vault」「更多工具」互斥打开其一检查器关闭会清掉紧凑态。宽窗仍同时显示 Vault检查器由inspectorOpen控制。随手记同样按需切换文档库/快速替换工具栏可改当前笔记fontName/fontSize/syntax并立即作用于EditorHost。搜索与设置快捷键冲突时字段旁显示错误绑定不写入设置。二、系统字体选择从回退字体到本机字体枚举2.1 字体枚举的核心实现SystemFontsDIFF-035 的字体选择建立在 SystemFonts.kt 之上。SystemFonts.list()的构建策略是「先回退、后枚举、再兜底」先把一份回退字体表放入LinkedHashSet保证跨平台一致的候选顺序ui-monospace、Menlo、Monaco、Consolas、Courier New、JetBrains Mono等宽字体族PingFang SC、Hiragino Sans GB、Microsoft YaHei、等线、Songti SC、SimSun中文正文/标题族Georgia、Times New Roman、SF Pro Text、Helvetica Neue、Arial、system-ui通用字体族。通过GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames枚举当前 JVM 真正可用的字体族逐个加入集合。把当前选中值current如设置里已保存的jsonFontName也加入集合保证「当前字体一定出现在列表里」即使它既不在回退表也不在本机字体列表。排序时把ui-monospace固定在首位其余按大小写不敏感字典序排列。SystemFonts.displayName(value, labels, emptyLabel)负责显示名解析有标签映射用标签如ui-monospace显示为「等宽字体」否则回退到字体族名本身值为空时显示emptyLabel如「系统字体」。2.2 与 Electron 的归一化对齐字体值在写入设置前还会经过 EditorFontSettings.kt 的归一化fun normalizeFontName(value: String, fallback: String): String { val trimmed value.trim().take(120) return trimmed.ifEmpty { fallback } }该实现明确注释「与 ElectronnormalizeFontNamenext/src/shared/contracts/settings.ts对齐」去除首尾空白、截断到 120 字符、空值回退到默认值。normalizeEditorSettings会把jsonFontName、quickNoteFontName一并归一化保证从 Electron 设置或旧版 Java 设置导入的数据不会出现非法字体名。2.3 设置默认值与持久化Settings.kt 中的EditorSettings给出默认值Serializable data class EditorSettings( val sqlDialect: String Standard SQL, val jsonFontName: String ui-monospace, val jsonFontSize: Int 14, val quickNoteFontName: String ui-monospace, val quickNoteFontSize: Int 14, val softWrap: Boolean true )即JSON 与随手记的字体默认均为ui-monospace字号默认 14软换行默认开启。设置页与工具页写入的是同一个键因此「JSON 工具栏改字体」与「设置页改字体」是同一存储位置。2.4 统一控件FontSelectJSON 工具栏与设置页共用的 FontSelect.kt 是一个带搜索的下拉面板按钮标题为ariaLabel · display如「JSON 字体 · 等宽字体」展开后弹出 280dp 宽的 Popup含可选emptyLabel项点击后写入空字符串以回退系统字体、MooTextField搜索框以及 240dp 高的LazyColumn字体列表搜索采用大小写不敏感包含匹配it.contains(query, ignoreCase true)选中项以colors.accent高亮点击后通过onChange(font)写回设置并关闭弹层。由于val fonts remember(value) { SystemFonts.list(value) }以value为 key当选中的字体变化时列表会重新计算确保新选字体出现在首屏。2.5 工具栏到编辑器的即时生效JSON 页面在 JsonScreen.kt 中把settings.editor.jsonFontName通过DocumentFormatEngine.editorFont(...)处理后传给EditorHost的fontNamejsonFontSize经EditorSettingsLiveApply.jsonEditorFontSize(...)实时套用。随手记侧同理见下文第四节。三、960 紧凑布局与按需辅助栏3.1 布局判定的唯一出口LayoutPolicyDIFF-035 把「什么宽度下折叠什么」全部收敛到 LayoutPolicy.kt用常量与纯函数表达const val COMPACT_WIDTH_DP 960f const val COMPACT_HEIGHT_DP 640f const val FULL_TOOLBAR_WIDTH_DP 1440f const val JSON_DESKTOP_INSPECTOR_WIDTH_DP 1321f fun isCompact(contentWidthDp: Float): Boolean contentWidthDp COMPACT_WIDTH_DP fun jsonInspectorDesktopOpen(contentWidthDp: Float): Boolean contentWidthDp JSON_DESKTOP_INSPECTOR_WIDTH_DP fun collapseNavigation(contentWidthDp: Float, sidebarCollapsed: Boolean, hideTitles: Boolean): Boolean sidebarCollapsed || hideTitles || isCompact(contentWidthDp)关键断点内容宽 960dpisCompact true导航无条件折叠注意断点是contentWidthDp 960f即 959 以下算紧凑960 及以上算宽窗内容宽 640dp 高showMinSizeHint true提示用户窗口过小内容宽 1440dp工具栏溢出overflowToolbar true内容宽 ≥ 1321dpJSON 检查器在桌面端默认打开与 ElectronJsonTool的(min-width: 1321px)断点对齐。3.2 辅助栏互斥状态机紧凑态下Vault、检查器、快速替换不能同时占位挤掉编辑器因此引入compactAux单值状态机。toggleAux的语义是「点同一个目标再点一次即关闭」fun toggleAux(current: String, target: String): String if (current target) else target fun showVault(compact: Boolean, compactAux: String): Boolean !compact || compactAux vault fun showInspector(compact: Boolean, compactAux: String, inspectorOpen: Boolean): Boolean if (compact) compactAux inspector else inspectorOpen fun showReplace(compact: Boolean, compactAux: String, replaceOpen: Boolean): Boolean if (compact) compactAux replace else replaceOpen fun showQuickNoteVault(compact: Boolean, compactAux: String, vaultTreeOpen: Boolean): Boolean vaultTreeOpen showVault(compact, compactAux)四个函数可总结为一句话规则宽窗下各面板由各自的*Open开关独立控制紧凑态下只认compactAux这一个字段且互斥。随手记的文档库还额外要求vaultTreeOpen为真宽窗下treeOpen决定显示窄窗下仍走compactAux见代码注释。3.3 JSON 页面的应用JsonScreen.kt 中内容宽变化时通过LayoutPolicy.isCompact(maxWidth.value)计算compactoverflowToolbar决定工具栏是否折叠多余项桌面端检查器默认开合由LayoutPolicy.jsonInspectorDesktopOpen驱动且通过LaunchedEffect(compact, desktopInspectorOpen)在跨档切换如 1320→1321时把结果写回session.inspectorOpen并持久化宽窗下showVault恒为 true!compact所以 Vault 与检查器可以同时出现检查器单独受inspectorOpen控制工具栏中toggleInspector()分两支紧凑态写compactAux宽窗态翻转inspectorOpen见 JsonScreen.kt「JSON Vault」按钮以primary session.compactAux vault高亮点击后执行LayoutPolicy.toggleAux(session.compactAux, vault)检查器关闭时若处于紧凑态会一并清空compactAuxif (session.compactAux inspector) session.compactAux 避免留下「已关闭却仍标记为激活」的脏状态。3.4 随手记页面的应用QuickNoteScreen.kt 使用同一套策略工具栏「快速替换」「文档库」按钮通过LayoutPolicy.toggleAux(session.compactAux, ...)互斥切换面板显示分别走LayoutPolicy.showReplace与LayoutPolicy.showQuickNoteVault。因此 JSON 与随手记在 960 窄窗下的行为完全一致一次只显示一个辅助栏编辑器永远占满剩余空间。四、随手记字体会话即时生效与 frontmatter 持久化4.1 工具栏三控件的实时作用随手记工具栏提供字体、字号、语法三项控制QuickNoteScreen.kt字体FontSelect的值来自session.metadata.fontName.ifBlank { settings.editor.quickNoteFontName }选择后写入session.metadata.fontName字号session.metadata.fontSize 0 ? session.metadata.fontSize : settings.editor.quickNoteFontSize菜单里可选多个尺寸并写入metadata.fontSize语法session.metadata.syntax.ifBlank { text/plain }菜单选择后同时更新session.metadata.syntax与session.editor.syntax DocumentFormatEngine.rstaSyntax(id)。渲染端QuickNoteScreen.kt把fontName/fontSize传给EditorHost配合DocumentFormatEngine.editorFont(...)完成立即作用于编辑器的显示效果——不改全局设置、不动其他笔记。4.2 保存时写入 frontmatter字体、字号、语法属于笔记级 metadata保存时由 QuickNoteVaultContext.kt 归一化并落盘val metadata session.metadata.copy( title session.metadata.title.ifBlank { ... }, syntax session.metadata.syntax.ifBlank { NoteFrontmatter.syntaxForExtension(...) }, lineWrap session.wrap, fontName session.metadata.fontName.ifBlank { container.settings.value.editor.quickNoteFontName }, fontSize session.metadata.fontSize.takeIf { it 0 } ?: container.settings.value.editor.quickNoteFontSize ) return runCatching { vault.saveNote(name, session.editor.text, metadata) }序列化由 NoteFrontmatter.kt 的serialize完成其中font_name字段由yamlScalar(normalized.fontName)输出读取时从values[font_name]解析回 metadata。NoteVault.saveNote内部先NoteFrontmatter.normalize(...)fontName截断 120 字符再写盘。换言之已保存的笔记字体在下次打开时从 frontmatter 恢复按笔记各自生效未保存的笔记字体只存在当前会话session.metadata里关闭即丢不影响全局设置。五、快捷键冲突的可见提示5.1 冲突判定ShortcutBindingsShortcutBindings.kt 提供两个纯函数fun conflict(first: String, second: String): Boolean { val left normalize(first) // trim val right normalize(second) return left.isNotEmpty() right.isNotEmpty() left.equals(right, ignoreCase true) } fun formatDisplay(value: String, mac: Boolean isMac()): String { val meta if (mac) ⌘ else Ctrl return normalize(value) .replace(CommandOrControl, meta, ignoreCase true) .replace(Meta, meta, ignoreCase true) .replace(Comma, ,, ignoreCase true) }冲突规则为两侧 trim 后非空且忽略大小写与首尾空白后完全相等即冲突。因此MetaK与 metak 冲突MetaK与MetaComma不冲突空串不参与冲突。formatDisplay负责把存储格式转成平台化显示macOS 上Meta显示为⌘Windows/Linux 显示为CtrlComma显示为,。5.2 设置页的可见错误流SettingsScreen.kt 的 Shortcuts 分组维护shortcutError状态用户输入搜索快捷键时next value.trim().ifBlank { MetaK }若ShortcutBindings.conflict(next, settings.shortcuts.settings)为真设置shortcutError t(settings.shortcuts.conflict)不调用updateSettings即绑定不写入设置反之清空错误并写入settings.shortcuts.search设置快捷键的输入框对称地以settings.shortcuts.search为对手做同样校验错误文案以colors.danger显示在字段下方用户可立即看到冲突原因。这正对应文档强调的「快捷键冲突必须可见不能静默丢输入」——Electron 基线虽已「拒绝覆盖并给出原因」Compose 侧用同一语义复刻并配ShortcutBindingsTest固化行为。六、证据链与验收边界6.1 自动化证据DIFF-035 的验收证据分三层详见 evidence 结果页单测desktopTest193/193全绿0 skippedSystemFontsTest.kt断言list首位恒为ui-monospace、包含Menlo、包含当前选中值且剩余列表大小写不敏感有序ShortcutBindingsTest.kt断言忽略大小写/空白的冲突判定以及⌘K/CtrlK的平台化显示LayoutPolicyTest.kt断言 959 为紧凑、960 为宽窗toggleAux/showVault/showInspector/showReplace的互斥组合以及jsonInspectorDesktopOpen的 1321 断点与 Electron 对齐。Compose 场景 PNGCompactShellCaptureTest.kt 在 960×640 测试画布内渲染「JSON JSON Vault」折叠壳捕获compact-960-focus-ring.png并做像素断言检测聚焦按钮的 focusRing 色0x316DC0命中数 ≥ 8。注意该图是测试场景渲染图不是真实窗口截图。JFrame.paint辅助图CompactWindowPaintTest.kt 用 AWTJFrameComposePanel在 960×640 下frame.paint(graphics)生成jframe-paint-960.png同样不是用户窗口手势截图。6.2 明确未覆盖项文档如实列出验收缺口IME输入法场景、列编辑的手工窗口验证三平台Win/macOS/Linux的安装、升级、卸载流程六套 CSS 皮肤逐选择器核对。这意味着 DIFF-035 的像素级验收依赖 JVM 侧的自动渲染图真实窗口/安装层的最终确认仍待后续补充阅读时不宜把这些 PNG 当作成品截图。七、受影响的回归点紧凑宽度下必须点工具栏才能看到 Vault/检查器/快速替换且一次只显示一个辅助栏——若窗口小于 960dp左右面板默认全隐藏这是有意的信息架构变化不是 bug。随手记字体写入文档 metadata保存后进入 frontmatterfont_name字段未保存的字体只在当前会话生效。JFrame.paint与 Compose 场景 PNG 不能代替真实窗口截图验收——这是该模块当前已知的证据边界。结语DIFF-035 以「同一语义、两处统一」贯穿全局字体选择由SystemFonts.listFontSelectEditorFontSettings.normalizeFontName统一960 紧凑布局由LayoutPolicy的纯函数统一快捷键冲突由ShortcutBindings.conflict/formatDisplay统一。源码结构显示JSON 与随手记共享同一套按需辅助栏状态机设置页与工具栏共享同一个字体存储键这在多端产品中是值得复用的收敛模式。对于要维护或移植该模块的开发者建议以desktopTest下的SystemFontsTest、ShortcutBindingsTest、LayoutPolicyTest三个测试类为行为契约以docs/evidence/2026-09-15-compact-font-shortcuts/results.md为验收基线。赞分享桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载相关推荐MooTool Next Compose 编辑器宿主架构解析RSyntaxTextArea SwingPanel 的 ADR-002 决策与实践MooTool Next Compose 编辑器宿主架构解析RSyntaxTextArea SwingPanel 的 ADR 002 决策与实践 导读 本桌面应用开发工具MooTool Next Compose 验收体系状态规则、DIFF 证据链与 1344 测试基线的复现方法MooTool Next Compose 验收体系状态规则、DIFF 证据链与 1344 测试基线的复现方法 本文以 MooTool Next Compose桌面应用开发工具MooTool Next Compose 壳层主题、编辑器语法色、历史隐私与安装声明实战指南DIFF-033 全解析MooTool Next Compose 壳层主题、编辑器语法色、历史隐私与安装声明实战指南DIFF 033 全解析 本文依据 MooTool Next C桌面应用开发工具上一篇10分钟上手OpenToonz用免费开源2D动画软件画出第一帧并加上运动模糊下一篇如何安装和配置ruby-prof面向初学者的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考