- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
本文围绕 MooTool 仓库中的差异记录文档next-compose/docs/diff/056-command-palette-sidebar-search.md(DIFF-056),讲解 Compose 桌面版(next-compose)如何对齐 Electron 版(next)的两处壳层体验:一是把命令搜索(A02 命令盘)从“仅快捷键可用”升级为侧栏可见入口,二是让命令盘弹层获得与 Electron 一致的 chrome 规格(620dp 宽、描边阴影、分组标注、键盘走查)。读完本文,你将掌握这套实现的组件结构、焦点与按键处理链路、对应的单元测试证据,以及它与 Electron 原实现之间明确的边界差异。
一、背景:Electron 版的原行为
DIFF-056 记录的是 Compose 版与 Electron 版之间的行为差异及弥合方式。文档给出的 Electron 原行为基线是:
- 侧栏
sidebar-actions依次为「折叠」「搜索(tooltip搜索 · ⌘K)」「管理分组」三个图标按钮; CommandPalette为 620px 弹层:搜索行带关闭按钮,结果行显示工具名与分组,选中行使用--control-active底色,空结果文案居中;输入框内支持 ↑↓/Enter/Esc;- 导航
.tool-button始终带 1px 边框,悬停/选中时有 raised 底色。
这些描述都能在 Electron 版源码中得到印证:
- 侧栏入口位于 Workbench.tsx:
sidebar-actions容器内依次渲染折叠按钮(PanelLeftClose/PanelLeftOpen)、搜索按钮(Search图标,onClick调用setSearchOpen(true))、管理分组按钮(FolderCog)。其中搜索按钮的 tooltip 为`${t('app.nav.search')} · ${window.mootool.platform === 'darwin' ? '⌘K' : 'Ctrl+K'}`,即文档所说“macOS 为搜索 · ⌘K,其他为Ctrl+K”的原始出处(Electron 版同样做了平台区分)。 - 快捷键绑定在同文件的全局 keydown 监听中(Workbench.tsx):
(metaKey || ctrlKey) && key === 'k'时setSearchOpen(true);托盘菜单则通过导航事件focus-search打开命令盘(index.ts 中openCommandPalette: () => navigateMainWindow('focus-search'),以及 trayMenu.ts 中的commandPalette菜单项)。 - 弹层规格在 global.css:
.command-palette宽度min(620px, calc(100vw - 48px)),带 1px 描边、border-radius: 8px与0 24px 70px阴影;选中行.command-result--selected使用background: var(--control-active);空结果.command-palette__empty以text-align: center居中。 - 键盘处理在 CommandPalette.tsx:输入框聚焦时
ArrowUp/ArrowDown移动selectedIndex(带Math.max(0, Math.min(..., results.length - 1))边界钳制),Enter选中当前项,Escape关闭,Tab在弹层内做焦点环绕。
文档同时给出了弥合理由:ui-spec 壳图与 Electron 都把搜索做成侧栏可见入口,而 Compose 版原先只有快捷键;此外输入框吞掉方向键时命令盘无法完成键盘走查。这两点正是后文 Compose 实现要解决的核心问题。
二、Compose 版侧栏搜索入口
Compose 版主壳位于 Sidebar.kt。文档“本产品行为”第一条——“侧栏标题栏顺序改为折叠、搜索⌕、分组+”——对应源码中工具栏 Row 内连续的三个SidebarGhost按钮(Sidebar.kt):
SidebarGhost( label = container.t(if (collapsed) "app.nav.expand" else "app.nav.collapse"), glyph = if (collapsed) "»" else "«", onClick = onToggle ) SidebarGhost( label = searchTooltip(container.t("app.nav.search")), glyph = "⌕", onClick = { container.setSearchOpen(true) } ) SidebarGhost( label = container.t("app.nav.manageGroups"), glyph = "+", onClick = { container.setGroupManagerOpen(true) } )几个与文档一一对应的细节:
- 搜索图标采用文本字形
⌕(而非图标库),分组按钮用+,与文档“搜索⌕、分组+”的描述一致; - 搜索按钮 tooltip 由
searchTooltip(...)生成,其实现是"$search · ${if (mac) "⌘K" else "Ctrl+K"}"(Sidebar.kt),完整继承了 Electron 版的平台化文案; - 点击后调用
container.setSearchOpen(true)打开命令盘;快捷键一侧则由全局按键匹配解析meta/cmd/ctrl + k绑定(Workbench.kt 中的matchesShortcut),保证“仅快捷键”与“侧栏入口”两条打开路径共存。
文档还要求“搜索与折叠/分组同一套焦点环”。从源码结构看,三个SidebarGhost走同一 ghost 按钮组件与焦点样式体系,因此悬停、键盘聚焦时的视觉反馈保持一致;这一点与 ui-spec 壳“折叠 / 搜索 / 分组”三件套的要求相符。
三、命令盘弹层 chrome:620dp、描边阴影与分组标注
命令盘本体是 Workbench.kt 中的CommandSearchcomposable,文档中“本产品行为”的弹层条款逐条落实如下:
- 宽度 620dp、描边与阴影:弹层
Column的 Modifier 为Modifier.width(620.dp).mooDialogSurface(MooTheme.dimens.commandRadius)(Workbench.kt),mooDialogSurface提供对话框级描边与阴影,对齐 Electron 的 620px.command-palette。 - 搜索行:高度 54dp 的 Row,左侧
⌕字形、中间MooTextField(占位符app.search.placeholder)、右侧关闭按钮。关闭按钮是一个×文本,通过Modifier.focusRequester(closeFocus)参与 Tab 走查,点击或聚焦回车时container.setSearchOpen(false);其语义contentDescription取app.search.close(Workbench.kt),即文档所称“关闭按钮app.search.close”。三语文案在 CommandPaletteKeysTest.kt 中有断言:中文“搜索工具 / 搜索功能… / 关闭搜索”,英文 “Search tools”,日文 “ツールを検索”。 - 结果显示本地化名称与分组名(首页无分组):结果行渲染工具本地化标题(
container.t(titleKey))与右侧 11sp 的分组名(ToolRegistry.groupTitleKey(entry.definition.groupId),Workbench.kt)。groupKey为可空值,对应“首页(mootool)无分组”的行为——首页工具查不到分组标题时该列留空。 - 选中
selected底:结果行背景由colors.sidebarItemBrush(active, ...)决定,active即index == selected,对应文档“选中selected底”;同时ToolIcon在选中时切换为colors.navSelectedIcon()。 - 空文案居中:
results.isEmpty()时渲染app.search.empty文案,TextAlign.Center且上下 44dp 内边距(Workbench.kt),与 Electron 版.command-palette__empty的居中布局等价。 - 打开时焦点进输入框:
LaunchedEffect(Unit) { searchFocus.requestFocus() }(Workbench.kt)保证弹层一打开键盘焦点就落在搜索框。
四、键盘走查:方向键钳制与 Tab 焦点链
文档特别指出“输入框吞掉方向键时命令盘无法键盘走查”,Compose 版的解法是把按键处理放在弹层Column的onPreviewKeyEvent上(先于输入框消费),并按焦点位置分发。核心是纯函数onPaletteKey(Workbench.kt),规则为:
Tab:调用commandPaletteTabFocusTransition(shift, from, resultCount)在Search → Close → Result三个CommandPaletteFocusTarget之间移动焦点,返回null时放行走默认 Tab 行为;- 焦点在
Search:↑/↓仅在“有结果”时改变selected(这正是“输入框内方向键也能换行选择”的实现),Enter触发choose(selected),Esc关闭; - 焦点在
Close:Enter关闭弹层; - 焦点在
Result:↑/↓/Enter/Esc均直接操作结果列表,不受输入框影响。
方向键索引移动统一走nextCommandIndex(Workbench.kt):
internal fun nextCommandIndex(selected: Int, size: Int, down: Boolean = true, stay: Boolean = false): Int { if (size <= 0) return 0 if (stay) return selected.coerceIn(0, size - 1) return if (down) (selected + 1).coerceAtMost(size - 1) else (selected - 1).coerceAtLeast(0) }即向下到末尾停在末尾、向上到 0 停在 0,stay模式用于结果集变化后把旧选中值钳回合法区间(见LaunchedEffect(results) { selected = nextCommandIndex(selected, results.size, stay = true) })。
Tab 焦点链的完整状态机由commandPaletteTabFocusTransition实现(Workbench.kt):从Search正向 Tab 到Close;从Close正向 Tab 到Result(仅当有结果);从Result需 Shift+Tab 才回到Close(有结果时)。该设计与 Electron 版 CommandPalette.tsx 中“Tab 在弹层可聚焦元素内环绕”的意图一致,但以显式焦点目标建模,便于单测。
五、证据:单元测试与 26 项基线
DIFF-056“证据”一节列出的两条验证,都能在仓库中找到:
CommandPaletteKeysTest(CommandPaletteKeysTest.kt)覆盖四个测试:arrowsClampInsideResults:方向键在首/尾的钳制行为(nextCommandIndex(0, 0, down=true) == 0、(0,3,down) == 1、(2,3,up) == 1等);tabFocusTransition_movesSearchCloseAndResult:Tab/Shift+Tab 在 Search/Close/Result 之间的转移,含“无结果时 Close 之后无去向(返回null)”等边界;tabFocusWalkthrough_searchCloseResultRoundTrip:正向走查链应为[Close, Result](5 个结果)或[Close](0 个结果),反向链应为[Close, Search];searchCopyFollowsElectronKeys:三语搜索标题/占位符/关闭文案与 Electron 一致。
ToolRegistry空查询返回 26 项:ToolRegistryTest.kt 中assertEquals(26, ToolRegistry.tools.size)与assertEquals(26, ToolRegistry.search("") { it }.size)两处断言确认命令盘空查询时列出全部 26 个工具(含设置入口在内的条目由commandSearchEntries组织)。
文档还记录当时desktopTest全量 240/240 通过,以及三张主窗截图证据:71-home-search-button.png(侧栏搜索入口)、72-command-palette.png(命令盘弹层,见文首配图)、73-group-manager.png(分组管理弹层),位于 docs/evidence/2026-09-15-inspector-screencapture/windows/ 目录。
六、导航行 hoverable 与风格差异
文档“本产品行为”第三条要求导航行支持hoverable:悬停/选中时 1dp 边框,且在 smartisan/hero/miui-v5 风格下悬停用 raised 渐变。实现位于侧栏NavItem(Sidebar.kt):
- 通过
remember { MutableInteractionSource() }同时跟踪hovered与focused状态,并挂载Modifier.hoverable(interaction).focusable(true, interaction); - 边框宽度与颜色按状态切换:
focused -> 2.dp to colors.focusRing,否则1.dp to colors.navItemBorder(selected, hovered)——即平时(含悬停、选中)始终 1dp 边框、聚焦升级为 2dp 焦点环,对齐 Electron 版“.tool-button始终 1px 边框”的规格; - 底色由
colors.sidebarItemBrush(selected, card, hovered)按styleId提供,smartisan/miui-v5 等风格下悬停即得到 raised 渐变底;smartisan/miui-v5 选中时还会在行首绘制 3dp×18dp 的navActiveBar指示条(Sidebar.kt)。命令盘的搜索行同样在smartisan/miui-v5风格下应用toolbarBrush()背景(Workbench.kt),保证弹层 chrome 跟随全局风格。
七、分组弹层:只在打开时快照草稿
文档最后一条行为:“分组弹层只在打开时快照草稿,编辑过程中不再被设置流重置。”其实现见 CustomGroupManager.kt:
val initial = container.settings.value.layout.customGroups var groups by remember { mutableStateOf(CustomGroupDraft.copyOf(initial)) }remember只在该 composable 首次进入组合时执行一次,因此groups是打开时刻的CustomGroupDraft副本(copyOf深拷贝),后续用户在弹层内新增/改名/重排分组只改这份草稿,不会再被设置流中的其他变更重置;关闭时(MooOverlay的onDismiss调用container.setGroupManagerOpen(false))或提交时才把结果写回设置。这个“打开即快照”的语义使分组编辑与会话中的其他设置变更互不干扰,是 DIFF-056 顺手修正的一个状态污染问题。
八、受影响范围与适用限制
DIFF-056 同时明确了本次弥合没有覆盖的内容,使用与移植时应注意:
- Compose 版仍不是 Electron 六套 CSS 的逐选择器皮肤:Electron 侧对
.command-palette存在data-interface-style为hero/smartisan/miui-v5/claude等风格的整套覆盖规则(见 global.css 中的:root[data-interface-style='...'] .command-palette系列选择器),而 Compose 版目前只在部分风格(如 smartisan/miui-v5 的工具栏底色)做了对应,并非逐选择器复刻; - 主窗 Tab/IME 行为为手工验证;托盘 TCC(截屏权限)与三平台安装包路径未在本差异范围内验证(文档“受影响范围”原文列出)。
因此,若参考本文在 Compose 桌面版上定制命令盘,可依赖的稳定契约是:侧栏三按钮顺序与 tooltip 规则、620dp 弹层 chrome、Search/Close/Result三焦点目标 Tab 链、方向键钳制函数,以及 CommandPaletteKeysTest.kt 与 ToolRegistryTest.kt 所锚定的键盘与数据基线(空查询 26 项);风格化外观则需以 ui-spec 壳图为准另行评估。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool Next Compose 分离窗口聚焦、侧栏分离入口与最小尺寸提示实现解析(DIFF-042)
MooTool Next Compose 分离窗口聚焦、侧栏分离入口与最小尺寸提示实现解析(DIFF 042) 本文基于 MooTool 仓库中的 DIFF 0
桌面应用开发工具MooTool next-compose:1080 窄屏工具栏溢出与 compact 下拉实现(DIFF-049)
MooTool next compose:1080 窄屏工具栏溢出与 compact 下拉实现(DIFF 049) 本篇围绕 MooTool 仓库中 next
桌面应用开发工具MooTool Next Compose 文本对比同步滚动、Git 前后分栏与 1080 工具栏溢出实现解析(DIFF-043)
MooTool Next Compose 文本对比同步滚动、Git 前后分栏与 1080 工具栏溢出实现解析(DIFF 043) 导读 本文以 MooTool
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考