news 2026/10/15 16:53:02

MooTool 应用内遮罩弹层(DIFF-057)实现解析:命令盘与分组管理器的 Compose Overlay 实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MooTool 应用内遮罩弹层(DIFF-057)实现解析:命令盘与分组管理器的 Compose Overlay 实践
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

MooTool 的桌面版基于 Compose Multiplatform 重写(next-compose),其中"命令盘/分组弹层改为应用内遮罩"(DIFF-057)记录了搜索与分组管理弹层从系统半透明窗口迁移到主窗口内固定遮罩的关键决策。本文以该设计文档为骨架,结合 MooOverlay.kt、Workbench.kt、ThemeContrastTest.kt 等源码与测试,完整还原其设计背景、遮罩配色体系、布局规则、交互行为与配套验证,并给出可直接检索与复用的实现要点。

一、变更背景:Electron 原行为与本产品目标

1.1 Electron 的固定遮罩

在 Electron 版 MooTool 中,命令搜索(command palette)与分组管理均以应用壳内 fixed 遮罩呈现:

  • 遮罩色为rgba(0,0,0,.2),深色主题或 hero 风格下更深;
  • 面板使用surface-elevated的不透明表面;
  • 点击遮罩即可关闭弹层;
  • 这些弹层不是独立透明系统窗,始终位于应用主窗内。

对应 CSS 选择器为 Electron 侧的command-palette-backdrop与.dialog。

1.2 本产品(Compose 重写版)行为

next-compose遵循同一设计目标,但落地为全新的 Compose 实现:

  • 弹层画在 Workbench 内,由MooOverlay承载,遮罩来自overlayScrim()(浅色 0.20 / 深色 0.46),点击遮罩关闭;
  • 命令盘靠上(距顶约 96dp),分组管理器居中;面板一律用workspace实底;
  • 删除确认仍是第二层遮罩(叠加在分组管理器之上),不关闭管理器本身;
  • 彻底弃用 ComposeDialog弹出独立半透明窗的做法(此前主窗截图会出现命令盘/分组与首页叠在一起的问题)。

该变更影响两个核心区域:A02 命令搜索与 A01 分组管理器。

二、为什么不用 Compose Dialog:技术决策依据

设计文档给出的理由非常直接:

Electron 弹层属于壳,不离开主窗口。系统Dialog在 macOS 上半透明,截到的命令盘/分组与首页叠在一起,也不符合点遮罩关闭。

也就是说,选择应用内遮罩而非系统Dialog有三个现实原因:

  1. 截图穿透:macOS 上 ComposeDialog是独立半透明窗口,主窗内容会透出下层,导致命令盘/分组与首页视觉叠加;
  2. 交互不符:系统窗口与"点遮罩关闭"的应用内行为不一致;
  3. 层级控制:应用内遮罩能精确控制与侧栏、工具区、Swing 编辑器的 z-order 关系(详见后文ModalOverlayState机制)。

2.1 同类演进:工具内 Dialog 同样被替换

这条设计路线并非只针对命令盘与分组管理器。受影响的后续变更包括:

  • DIFF-059:JSON / 随手记 / HTTP / 图片 / 调色板 / Host / 环境变量 / 翻译 / 运行台 / PDF / 正则 / Cron / 时钟 的 ComposeDialog全部改为MooOverlay;
  • DIFF-058:历史 / Vault Git / 冲突弹层也改为 overlay,并解决与 Swing 编辑器的互斥;
  • DIFF-060:主窗退出确认同样收敛为MooOverlay。

由此可见,MooOverlay是整套重写版桌面 UI 的统一弹层基元,而非一次性特例。

三、MooOverlay 实现剖析

MooOverlay.kt 是整个方案的实现核心。它对外暴露两个部分:ModalOverlayState(全局弹层计数)与MooOverlay组合函数。

3.1 ModalOverlayState:全局遮罩计数

object ModalOverlayState { var count by mutableStateOf(0) private set fun enter() { count++ } fun leave() { count = (count - 1).coerceAtLeast(0) } }

这个单例维护当前打开的应用内遮罩数量。MooOverlay在组合进入时调用enter(),在onDispose时调用leave(),形成生命周期对称。

该计数被 EditorHost.kt 消费:

if (ModalOverlayState.count > 0) { Box(modifier.fillMaxSize().background(colors.workspace)) } else { SwingPanel(factory = { buffer.scrollPane... }, ...) }

即:任何 overlay 打开时,EditorHost暂时卸下SwingPanel(AWT 编辑器),换成一个纯 Compose 的workspace色占位;关闭后恢复同一个EditorBuffer。原因是 Compose Desktop 的SwingPanel是重型 AWT 组件,会盖在同窗口的 Compose 遮罩之上抢走点击——这正是 DIFF-058 记录的"分组弹层盖在 JSON/HTTP 上时取消/保存点不到"问题的根治方案。

3.2 MooOverlay 组合函数

@Composable fun MooOverlay( onDismiss: () -> Unit, alignment: Alignment = Alignment.Center, contentPadding: PaddingValues = PaddingValues(24.dp), fillMaxSize: Boolean = false, content: @Composable () -> Unit )

关键行为:

参数/行为说明
onDismiss点遮罩或按 Esc 时回调,由调用方决定关闭哪个弹层
alignment面板对齐方式,命令盘用Alignment.TopCenter,分组管理器用默认居中
contentPadding面板外边距,命令盘传PaddingValues(top = 96.dp, ...)
fillMaxSize是否让面板占满整窗(工具内弹层常用)
遮罩Modifier.fillMaxSize().background(scrim).clickable(onClick = onDismiss)
EsconPreviewKeyEvent中拦截Key.Escape,并配合blockedByIme()避免 IME 输入法编辑时误关
面板实底、圆角、consumeClicks消费点击,防止点面板内部冒泡到遮罩关闭

源码结构上,MooOverlay是两层Box:

  • 外层全屏Box负责 Esc 拦截;
  • 内层第一个Box是遮罩,fillMaxSize().background(scrim).clickable(...);
  • 第二个Box按alignment或fillMaxSize布局面板,并用consumeClicks拦截面板自身的点击,避免误触关闭。

3.3 面板表面:mooDialogSurface

面板样式由 MooOverlay.kt 中的mooDialogSurface提供:

fun Modifier.mooDialogSurface(radius: Dp? = null): Modifier { val shape = RoundedCornerShape(radius ?: MooTheme.dimens.dialogRadius) val border = when (colors.styleId) { "smartisan", "miui-v5" -> colors.borderControl else -> colors.borderSoft } return this .shadow(24.dp, shape, ambientColor = colors.shadow, spotColor = colors.shadow) .clip(shape) .background(colors.workspace) .border(1.dp, border, shape) }

要点:

  • 实底:背景用colors.workspace(与 DIFF-060 对齐的 chrome token 体系);
  • 24dp 阴影 + 1dp 边框:视觉上区别于页面内容;
  • 边框 token 按风格差异化:smartisan / miui-v5 用borderControl,其余用borderSoft;
  • 圆角默认取MooTheme.dimens.dialogRadius(各风格不同,如 modern 13dp、hero 16dp、miui-v5 8dp,见 ThemeContrastTest.kt 的chromeRadiiMatchElectronStyleBlocks)。

四、遮罩配色体系:overlayScrim

4.1 六套风格的 alpha 设计

MooTheme.kt 的overlayScrim()是遮罩色的唯一来源:

fun overlayScrim(): Color { val dark = workspace.luminance() < 0.5f return when (styleId) { "hero" -> Color(0xFF09090B).copy(alpha = 0.46f) "smartisan" -> Color(0xFF231F1B).copy(alpha = 0.42f) "miui-v5" -> Color.Black.copy(alpha = 0.38f) "claude" -> Color(0xFF2D2520).copy(alpha = 0.34f) else -> Color(0xFF121214).copy(alpha = if (dark) 0.46f else 0.24f) } }

对应文档所述"浅色 0.20 / 深色 0.46"的精确实现:默认风格(modern / quiet 等)浅色遮罩 alpha 为 0.24,深色为 0.46;hero 在浅色下也达到 0.46(与 Electron 侧"深色/hero 更深"对应)。

各风格遮罩 alpha 汇总(由源码与 ThemeContrastTest.kt 交叉确认):

风格遮罩色alpha测试断言
modern(浅色)0xFF1212140.240.24f ± 0.02
modern(深色)0xFF1212140.46全部风格>= 0.2f,深色>= 0.34f
hero0xFF09090B0.46>= 0.4f(navSelectedTokensFollowElectronStyleBlocks)
smartisan0xFF231F1B0.42—
miui-v5Color.Black0.38—
claude0xFF2D25200.34—

4.2 对比度下限的自动化守护

ThemeContrastTest.kt 的bodyTextMeetsContrastFloorAcrossStyles对全部 6 套风格 × 明暗两态做校验:

val scrim = colors.overlayScrim() assertTrue(scrim.alpha >= 0.2f, ...) if (dark) assertTrue(scrim.alpha >= 0.34f, ...)

同时保证正文对比度textPrimary vs workspace >= 4.5、侧栏>= 4.5、焦点环>= 3。也就是说,遮罩 alpha 的下限是被测试锁定的,防止后续主题改动把遮罩调得过浅影响文字可读性。

这正是设计文档中"ThemeContrastTest校验各风格 scrim alpha、desktopTest 240/240"的证据所指。

五、命令盘:靠上居中的搜索浮层

命令盘是搜索弹层(CommandSearch),实现在 Workbench.kt。

5.1 调用方式

MooOverlay( onDismiss = { container.setSearchOpen(false) }, alignment = Alignment.TopCenter, contentPadding = PaddingValues(top = 96.dp, start = 24.dp, end = 24.dp, bottom = 24.dp) ) { Column( Modifier .width(620.dp) .mooDialogSurface(MooTheme.dimens.commandRadius) .onPreviewKeyEvent { onPaletteKey(it) } ) { // 搜索输入框 + 结果列表 + 关闭按钮 } }
  • 面板宽620dp,顶部距窗顶96dp(正是文档所说"约 96dp 靠上");
  • 圆角取commandRadius(modern 8dp、hero 16dp 等,见 ThemeContrastTest.kt);
  • 搜索结果实时根据 query 与当前语言重算:remember(query, container.settings.value.general.language) { commandSearchEntries(query) { container.t(it) } };
  • 选中项索引在结果变化时保持:LaunchedEffect(results) { selected = nextCommandIndex(selected, results.size, stay = true) }。

5.2 快捷键与焦点管理

命令盘实现了一整套键盘导航(onPaletteKey):

  • Key.DirectionDown / DirectionUp:移动选中项;
  • Key.Enter:执行选中命令(打开工具或设置分类)并关闭;
  • Key.Escape:关闭命令盘。

焦点管理通过CommandPaletteFocusTarget(Search / Close / Result 三区)与 Tab 迁移函数commandPaletteTabFocusTransition实现,支持 Tab / Shift+Tab 在搜索框、结果列表、关闭按钮间循环,函数commandPaletteTabForwardWalkthrough/commandPaletteTabBackwardWalkthrough还提供了测试友好的步进遍历。

5.3 打开与 Esc 短路

Workbench 顶层BoxWithConstraints挂onPreviewKeyEvent:

  • 命中settings.shortcuts.search快捷键(Electron 侧对应 Cmd/Ctrl+K)时setSearchOpen(true);
  • 命中Key.Escape时按优先级短路:searchOpen→groupManagerOpen→showSettings,逐层关闭。

由于快捷键挂在 Workbench 根布局(而非弹层内部),遮罩打开时这些快捷键依然生效,这一点与 DIFF-058 的记录一致。

六、分组管理器:居中的实底面板 + 二级删除遮罩

分组管理器CustomGroupManager实现在 CustomGroupManager.kt,结构是"一个居中 overlay + 一个条件渲染的二级 overlay"。

6.1 主面板

MooOverlay(onDismiss = { container.setGroupManagerOpen(false) }) { Column( Modifier.width(720.dp).heightIn(max = 560.dp).mooDialogSurface().padding(16.dp), ... ) { // 左侧:分组列表(新建/选中/数量) // 右侧:名称编辑 + 工具勾选(ToolRegistry 分组树) // 底部:删除 / 取消 / 保存(prominent,校验通过才可用) } }
  • 面板宽720dp、高上限560dp,alignment走默认Alignment.Center(文档"分组管理器居中");
  • 左侧分组列表 200dp,右侧工具勾选按ToolRegistry.groups的既有分组树渲染,每行一个MooSwitch;
  • 保存校验:CustomGroupDraft.invalid(groups)返回非空表示非法(名称空白或未勾选任何工具),此时"保存"按钮enabled = false,输入区下方同步显示danger色提示;
  • 保存时写回设置:container.updateSettings { current -> current.copy(layout = ...customGroups = CustomGroupDraft.persistable(groups)) }。

6.2 二级删除确认:不关闭管理器

删除确认是第二层MooOverlay,由pendingDelete状态控制:

if (pendingDelete && selected != null) { MooOverlay(onDismiss = { pendingDelete = false }) { Column( Modifier.width(420.dp).mooDialogSurface().padding(16.dp), ... ) { Text(container.t("app.group.manage.deleteConfirm", mapOf("name" to selected.name))) Row { MooButton(container.t("app.group.manage.delete"), danger = true, onClick = { val id = selected.id groups = groups.filterNot { it.id == id } selectedId = next.getOrNull(...)?.id pendingDelete = false }) MooButton(container.t("common.cancel"), onClick = { pendingDelete = false }) } } } }

行为要点(与文档完全对应):

  • 删除确认浮在管理器之上,形成第二层遮罩;
  • 点遮罩或"取消"只把pendingDelete置回 false,分组管理器保持打开;
  • 确认删除后移除对应分组,并把选中项迁移到相邻分组,随后关闭确认层。

由于ModalOverlayState.count为 2(两层同时打开),Swing 编辑器在此时也被一并卸下,两层 Compose 遮罩可正常叠放渲染。

七、设计红线与受影响范围

7.1 仍然不是六套 CSS 的逐选择器皮肤

文档明确:"仍非六套 CSS 逐选择器皮肤"。即本方案通过统一 token(overlayScrim()/mooDialogSurface/MooTheme.dimens)覆盖多风格外观,而不是逐选择器复刻 Electron 的command-palette-backdrop/.dialog样式。从 ThemeContrastTest.kt 的modernChromeTokensMatchElectronCss等测试可以看出,token 取值与 Electronglobal.css对齐,但组织方式是 token 化而非选择器移植。

7.2 工具内 Dialog 已由 DIFF-059 统一

命令盘/分组之后,所有工具内弹层统一走 DIFF-059,最终命令盘、分组管理器、工具弹层、历史、Vault Git、退出确认全部收敛到同一套MooOverlay。唯一保持原生的例外是系统FileDialog/JFileChooser/JColorChooser(见 DIFF-059)。

7.3 遗留未测项

设计文档记录的未测范围,在阅读源码时可视为验证边界:

  • 产品主窗 Tab / IME 手工交互;
  • 托盘 TCC(透明控制)场景;
  • 三平台安装包。

相关场景已在 Electron 侧或后续变更中覆盖(如 DIFF-060 记录了关闭 overlay 产品窗截图docs/evidence/2026-09-15-inspector-screencapture/windows/)。

八、可直接复用的实现清单

  1. 所有应用内弹层一律走MooOverlay,不再使用 ComposeDialog,避免 macOS 半透明系统窗导致的截图穿透与点击抢层;
  2. 遮罩色从overlayScrim()取,不要硬编码 alpha;改动主题时保证 alpha 下限与对比度断言(ThemeContrastTest.kt)不回归;
  3. 面板用mooDialogSurface:workspace实底 + 24dp 阴影 + 1dp 边框,圆角取dialogRadius/commandRadius;
  4. 依赖ModalOverlayState计数:overlay 打开时EditorHost自动卸下SwingPanel,关闭后恢复同一EditorBuffer,避免 AWT 编辑器遮挡 Compose 遮罩;
  5. 二级确认(如删除)用嵌套MooOverlay:通过独立状态(如pendingDelete)控制,点外层遮罩只关闭二级层,不关父弹层;
  6. 快捷键挂在 Workbench 根层:onPreviewKeyEvent里对 Esc 做searchOpen → groupManagerOpen → showSettings短路,保证遮罩打开时快捷键仍然有效;
  7. Esc 与 IME 协同:所有 Esc 处理都先过blockedByIme(),输入法组合编辑时不误关闭弹层。

九、验证与证据

  • 单元/集成测试:desktopTest在 DIFF-057 落地时为240/240通过(后续 DIFF-059/060 推进到 241/243,见各自设计文档);
  • 遮罩与 chrome token 断言见 ThemeContrastTest.kt;
  • 主窗重拍截图:72-command-palette.png(命令盘)、73-group-manager.png(分组管理器),尺寸 2880×1628,可直接核对弹层在真实主窗内的呈现(遮罩、靠上/居中布局、面板实底)。

延伸阅读:DIFF-058(overlay 与 Swing 编辑器互斥)、DIFF-059(工具内 Dialog 统一)、DIFF-060(退出确认与 chrome token)、architecture.md(SwingPanel 互操作说明)。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 16:51:46

流程控制语句深度解析:分支、循环与跳转的选型和易错点

这周的学习计划排得比较满&#xff0c;今天轮到流程控制语句。说实话&#xff0c;刚开始翻教材的时候觉得这块内容自己早就"会了"——无非就是 if 判断一下、for 循环几圈的事。但真正静下心来把分支、循环、跳转语句逐个过了一遍之后&#xff0c;我发现以前写代码时…

作者头像 李华
网站建设 2026/10/15 16:51:32

轻量级C++ JSON解析器TinyJson:设计原理与实战指南

1. 为什么你会需要一个轻量级的JSON解析器1.1 从C的选型困境说起提到C里解析JSON&#xff0c;很多人第一反应是去找那些功能全面的大库&#xff0c;它们确实好用&#xff0c;接口丰富、文档详尽、社区活跃。但如果你真的在资源受限环境里做过开发&#xff0c;比如MCU上的配置解…

作者头像 李华
网站建设 2026/10/15 16:51:10

Apollo-11:轻量级任务调度监控与告警系统实践拆解

Apollo-11 是我过去几周在团队内部推进的一个终端级监控与告警系统的项目代号。刚看到这个名字时你可能第一反应是登月&#xff0c;实际上我们就是希望它足够稳、足够可控&#xff0c;所以才借了这个代号。它解决的核心问题很明确&#xff1a;当我们把一套分布式的任务调度系统…

作者头像 李华
网站建设 2026/10/15 16:50:42

ISO/IEC 42001 草案落地指南:AI 管理体系从条款映射到运行监控

简介&#xff1a;ISO/IEC DIS 42001-2022&#xff08;draft&#xff09;是国际标准化组织与国际电工委员会联合发布的人工智能管理体系国际标准草案&#xff0c;面向AI治理、合规与信息安全从业者&#xff0c;以及需要搭建AI管理体系的组织。草案围绕组织与管理、风险管理、数据…

作者头像 李华
网站建设 2026/10/15 16:50:00

OpenOCD RAM调试实战:绕过Flash,把固件加载到SRAM运行

干嵌入式这一行&#xff0c;迟早会碰到一种需求&#xff1a;手头有一段代码&#xff0c;想临时跑一下验证逻辑&#xff0c;但又不想真往Flash里烧。尤其是调启动代码、调Flash算法、或者不小心把Flash保护位打开的时候&#xff0c;往Flash写一次都提心吊胆&#xff0c;搞不好就…

作者头像 李华