鸿蒙 PC Markdown 编辑器自由窗口:覆盖侧栏与响应式预算
鸿蒙 PC 应用不能假定一个固定窗口。用户会全屏、左右分屏、拖成窄长窗口,也会在写作时扩大侧栏查看长路径,再缩小窗口与资料并排。对 Markdown 编辑器而言,窗口适配不是把所有控件等比例缩小,而是明确哪些信息必须持续可操作,哪些可以临时覆盖,哪些可以在窄窗口隐藏,以及每个断点怎样保持焦点和文档状态。
本文分析 OhMarkdown 在提交 b11519c 中完成的 G3 自由窗口实现。公开仓库为 https://gitcode.com/VON-/codex_md_oh。本轮新增 900 vp 停靠/覆盖侧栏、760 vp 同步滚动控制预算、720 vp 状态栏精简和窄窗口冲突条换行,并保持 640 vp 最小编辑能力。Debug HAP 已编译,历史侧栏缩放已有模拟器证据;最新多档窗口仍因 Mac 锁定等待设备验收。
响应式设计要从任务优先级开始
主窗口包含活动栏、上下文侧栏、侧栏调整手柄、标签栏、打开与保存、视图模式、同步滚动、Web 编辑区、外部冲突条和状态栏。若只是给 Row 加 layoutWeight,宽度不足时 ArkUI 会继续压缩,最终出现文字截断、按钮重叠或编辑区只剩窄条。
OhMarkdown 把核心任务优先级定义为:源码编辑与保存永远可达;标签切换必须保留;视图模式在大文件之外可切换;侧栏可以临时覆盖;同步滚动开关可以在极窄窗口隐藏;编码与换行元数据可以暂时收起,但操作状态和字数保留;外部冲突决策不能消失。
这套优先级与 PRD 断点一致:1280 vp 以上适合完整工作台,960 至 1279 vp 保留左栏,720 至 959 vp 侧栏应临时显示,小于 720 vp 进入紧凑单栏。工程最终使用 900、760 和 720 三个断点,是结合当前组件实际宽度后的实现值,不把原型表机械当代码常量。
旧的"窄窗口直接隐藏侧栏"有什么问题
早期 shouldShowSidebar 只有一个判断:sidebarOpen && windowWidth >= 900。窗口小于 900 vp 时,侧栏从布局消失;但活动栏按钮仍能切换 sidebarOpen,用户点击后状态变了,界面却没有任何面板。这个实现避免编辑区被压缩,却让文件树、搜索、大纲和设置在分屏中不可用。
正确语义不是"窄窗口没有侧栏",而是"窄窗口侧栏不占据永久布局"。本轮拆成两个判断:
ts
private shouldDockSidebar(): boolean {
return this.sidebarOpen && this.windowWidth >= 900;
}
private shouldOverlaySidebar(): boolean {
return this.sidebarOpen && this.windowWidth < 900;
}
同一 sidebarOpen 状态在宽窗口表示停靠,在窄窗口表示覆盖。活动栏不需要知道当前模式,文件、搜索、大纲和设置也继续共用一份 contextualPanel。窗口跨过 900 vp 时面板自动从覆盖转停靠,不重建业务状态。
Stack 让覆盖侧栏不压缩编辑区
根布局从单一 Row 改成 Stack。底层 Row始终包含活动栏和编辑工作区,宽窗口按需在中间插入停靠侧栏;窄窗口时,第二层 Row从活动栏右侧开始绘制上下文面板和调整手柄,覆盖编辑区左部。
ts
Stack({ alignContent: Alignment.TopStart }) {
Row() {
this.activityRail()
if (this.shouldDockSidebar()) {
this.contextualPanel()
this.sidebarResizeHandle()
}
this.editorWorkspace()
}
.width('100%')
.height('100%')
if (this.shouldOverlaySidebar()) {
Row() {
Blank().width(ACTIVITY_RAIL_WIDTH)
this.contextualPanel()
this.sidebarResizeHandle()
}
.width(ACTIVITY_RAIL_WIDTH + this.sidebarWidth + SIDEBAR_RESIZE_HANDLE_WIDTH)
.height('100%')
.shadow({ radius: 18, color: '#26000000', offsetX: 6, offsetY: 0 })
}
}
覆盖层不放进装饰卡片,也不增加第二套侧栏组件。阴影只用于表达面板叠在编辑区上方,边界仍由原有分隔线负责。活动栏保持可见,点击当前活动项可以关闭覆盖层;用户不需要寻找浮层内部的额外关闭按钮。
640 vp 最小宽度怎样计算
根组件设置 minWidth: 640。活动栏固定 52 vp,编辑工作区至少约 588 vp;侧栏覆盖时不参与水平分配。若侧栏停靠,宽度限制函数始终预留至少 520 vp 编辑区,并把用户侧栏限制在 220 至 480 vp。
ts
private clampSidebarWidth(width: number, windowWidth: number = this.windowWidth): number {
const maxByWindow = Math.max(MIN_SIDEBAR_WIDTH,
windowWidth - ACTIVITY_RAIL_WIDTH - MIN_EDITOR_WORKSPACE_WIDTH - SIDEBAR_RESIZE_HANDLE_WIDTH);
return Math.max(MIN_SIDEBAR_WIDTH,
Math.min(MAX_SIDEBAR_WIDTH, maxByWindow, width));
}
在 640 vp 窗口中,maxByWindow 小于最小侧栏,于是覆盖层使用 220 vp,不压缩编辑区;在宽窗口中,用户可以拖到 480 vp,但仍不会吃掉核心编辑预算。窗口缩小时 onAreaChange 重新 clamp 当前值,扩大后 Preferences 中的已保存值会在下一次启动恢复;当前会话不会凭空扩回被截断前的值,这是可预期的保守策略。
覆盖层仍然允许调整宽度
覆盖侧栏继续显示 8 vp 调整手柄,鼠标悬停切换横向调整光标,PanGesture 更新宽度,键盘左右键按 16 vp 微调,结束后写 Preferences。覆盖模式的最大宽度同样受当前窗口和 220-480 vp 边界限制。
在很窄窗口中允许调整可能看似多余,但文件树路径、搜索选项和设置按钮对宽度需求不同。用户可以临时扩大覆盖面板完成查找,再点击活动栏关闭,编辑区完整恢复。拖动不会重载 ArkWeb 或重建文档会话,只改变 ArkUI 布局数值。
辅助功能把手柄公开为 Slider,提供"调整侧栏宽度"和当前 vp 描述。覆盖/停靠切换不改变语义,键盘用户不因分屏失去尺寸控制。
标签栏要让内容滚动而不是控件重叠
标签区域本身是水平 Scroll,拥有 layoutWeight(1);文档标签、加号在这条轨道内滚动,打开、保存、视图模式和侧栏按钮位于固定工具区。窗口变窄时,先减少可见标签数量,而不是压缩每个标签的关闭按钮和未保存标记。
单标签明确使用 Alignment.Start,不会在宽窗口中居中产生无效空白。固定 156 vp 标签宽度让文档名、脏标记和关闭按钮不会因动态内容改变工具栏尺寸。十二标签上限限制横向内容规模,用户仍可滚动找到每个会话。
760 vp 以下,分栏模式的"同步滚动"按钮暂时隐藏。同步状态仍保留,默认开启;用户扩大窗口后按钮恢复,状态不重置。选择隐藏它是因为模式切换比同步开关更基础,且同步开关只在分栏模式出现,若在极窄工具栏临时加入会造成标签宽度突然跳变。
状态栏精简要保留操作反馈
状态栏左侧是就绪、已修改、保存、导出、拖放和错误状态,右侧是字数、Markdown、编码与换行。小于 720 vp 时隐藏 Markdown、UTF-8/BOM 和 LF/CRLF/Mixed EOL,只保留操作状态与字数。
编码和换行很重要,但不是每一秒都需要操作;它们仍保存在文档格式模型中,扩大窗口后重新显示。操作状态不能隐藏,因为文件拖放、保存、导出或冲突失败需要即时反馈。字数体积小,也是写作任务的高频信息。
Text 使用单行省略和中间 Blank 分配剩余宽度,不让长错误消息覆盖右侧。未来可以给省略状态增加 Tooltip 或状态详情入口,但不能通过缩小字体塞入所有信息。
外部冲突条为什么必须换行
外部文件变化时,工作台显示比较、保留本地、使用磁盘和另存为四个动作。宽窗口中一行清晰;640 vp 下若继续一行,中文消息和四个按钮会相互挤压,最危险的"使用磁盘"可能只显示半个文案。
本轮把 Row 改为可换行 Flex。小于 900 vp 时,警告图标和消息占第一行,四个动作进入第二行,容器高度从 42 增至 74 vp;宽窗口保持单行。主编辑区会为冲突条让出明确高度,不会被浮层遮盖。
按钮高度、字体和颜色保持原设计,危险操作仍用警示色,另存为保持主动作绿色。响应式不是把所有按钮变成小图标,因为冲突决策需要清晰文字,误操作代价高于占用一行空间。
搜索面板还有自己的内部断点
窗口断点与侧栏内容断点不应混为一谈。侧栏可在 220-480 vp 调整,搜索的"区分大小写、全词匹配、正则表达式"在 300 vp 以下分两行,以上一行。即使窗口 1280 vp,用户把侧栏拖到 220 vp,搜索选项仍应响应;即使窗口 800 vp 覆盖侧栏,用户把它调宽,选项也可以恢复单行。
因此搜索布局依据 sidebarWidth,侧栏停靠/覆盖依据 windowWidth。两个变量分别描述局部容器和全局窗口,不使用同一个断点猜测。这种容器级预算能减少未来增加右侧面板时的耦合。
Web 编辑区也有独立窄布局
ArkWeb 内部的源码/预览分栏在页面宽度不足时转为垂直排列,CodeMirror 和预览各占一部分高度。ArkUI 负责给 Web 一个稳定矩形,Web CSS负责内部内容。两层不能都用 transform 缩放,否则字体、光标和滚动坐标会失真。
快捷键设置对话框使用 min(620px, 100%) 和最大视口高度,列表内部滚动;右键菜单在窗口边缘自动夹紧。这些 Web 浮层与 ArkUI 覆盖侧栏可以同时存在,但全局焦点路由确保 Escape 只关闭最上层任务。
自动化能证明什么,不能证明什么
Web Playwright 已覆盖窄窗口分栏模式,确认源码与预览垂直排列且没有横向溢出;本轮右键与快捷键面板也在真实生产单页截图中完整显示。Debug HAP 构建确认 Stack、FlexWrap、LengthMetrics、动态宽高和 Builder 均符合 API 24。
ArkTS 编译不能证明系统窗口拖到 640、720、760、900、1280 vp 时没有重叠,也不能证明系统标题栏、分屏边界和字体缩放的实际 vp。测试报告因此列出五个临界尺寸,要求每档执行打开侧栏、切换搜索、打开快捷键设置、切换分栏和触发冲突条,并保存截图。
窗口验收还应检查"跨断点保持状态":侧栏停靠状态、当前活动面板、侧栏宽度、同步滚动、标签会话和编辑正文不能在缩放时重置。只截五张静态图不足以证明状态连续,需要操作记录或连续截图。
自由窗口的性能风险
onAreaChange 在连续拖动窗口时频繁触发。当前回调只更新 windowWidth 并 clamp 一个数值,没有读磁盘、没有写 Preferences、没有执行 JavaScript,也不重新解析 Markdown。侧栏宽度只在用户结束手柄拖动时持久化,窗口缩放不会每帧 flush 设置。
ArkUI 条件分支会切换停靠与覆盖容器,但 contextualPanel 仍来自同一 Builder和状态。Web 组件始终位于底层 editorWorkspace,不因侧栏模式重建,避免 CodeMirror 光标、撤销历史和专业渲染代际丢失。
真正设备性能应观察窗口连续缩放时的帧率、ArkWeb 重排、预览滚动和内存。G3 目标首先保证无重叠和任务可完成,G4 再对 60fps、字体放大与长文档进行系统性能专项。
深色主题与覆盖层
覆盖侧栏继续使用资源化 surface、border 和文本颜色,跟随系统深色主题。阴影使用低透明黑色,在浅色与深色背景上都只表达层级,不创建彩色装饰。Web 右键与快捷键对话框也提供对应深色状态。
窗口缩放不改变主题,不触发专业渲染重建。主题变化才调用 ArkWeb setTheme,预览按当前代际重绘。把尺寸和主题两个状态分离,可以避免拖动窗口时反复执行 KaTeX 或 Mermaid。
可访问性与焦点
覆盖侧栏出现后,活动栏、面板和编辑区都仍在组件树中。用户点击活动栏关闭面板,键盘可通过已有焦点命名进入搜索输入。手柄拥有 Slider 角色和方向键操作。隐藏的同步滚动开关只在小于 760 vp 时暂不可见,不会留下不可见 tab stop。
冲突条换行后视觉顺序与组件顺序一致,Tab 依次到达比较、保留本地、使用磁盘和另存为。状态栏隐藏的元数据不参与焦点,本身也不是交互控件。快捷键设置作为模态对话框限制操作范围,Escape 归还 CodeMirror。
后续 G4 无障碍专项还需测试系统字体放大、屏幕阅读、焦点可见度和高对比,但 G3 响应式实现没有通过动态缩小字体解决空间问题,这为可访问性保留了基础。
文件安全不应因窗口变化而改变
侧栏从停靠变覆盖只改变布局,不改变工作区授权、活动文档、外部冲突或保存策略。窗口缩放不会自动保存,也不会清理恢复记录。冲突条即使换行,四个决策仍调用同一函数;没有为了窄窗口隐藏危险状态。
标签滚动不会卸载不可见会话,十二标签内容、脏标记和撤销历史继续存在。覆盖侧栏遮住部分编辑区时,用户关闭面板即可恢复;正文实际布局宽度未被永久修改。所有这些行为让自由窗口成为展示层变化,而不是文件生命周期变化。
竞争优势怎样验证
许多跨平台编辑器支持窗口缩放,OhMarkdown 不能仅凭"有响应式"宣称领先。统一任务应在 640、720、900、1280 和 1440 vp 下完成:打开文件树文档、搜索工作区、切换分栏、修改并保存、处理外部冲突、打开快捷键设置。记录每档完成率、控件重叠、额外操作数和窗口切换后的状态丢失。
当前自由窗口仍是 2 分:既有侧栏缩放已有模拟器证据,新覆盖层与多档预算已实现并编译,但缺少最新设备逐档截图。达到 3 分需要 MateBook Pro 2in1 模拟器通过临界窗口;达到 4 分需要真机、系统字体放大和竞品相同任务测量,并证明任务效率或稳定性优势。
多档设备验收应怎样记录
窗口测试最容易退化成几张看起来正常的截图,却遗漏实际不可操作状态。每个尺寸都应从同一初始文档开始,记录系统报告的 vp、侧栏模式、侧栏宽度、可见标签数、工具栏控件、状态栏字段和编辑区可用宽度。完成静态检查后,还要执行打开文件树、切到搜索、输入查询、回到源码、切换分栏、打开快捷键设置和保存文档,确保控件不仅没有重叠,而且可以获得焦点并完成任务。
临界值需要从两侧分别进入。899 vp 缩到 900 vp 与 901 vp 缩到 900 vp 可能触发不同的组件复用路径;759/760、719/720 同理。测试记录应保存缩放方向和跨越前后的状态,确认侧栏宽度、活动面板、搜索词、标签和正文没有重置。对于覆盖侧栏,还要检查鼠标点击编辑区是否仍能关闭或切换面板,阴影与分隔线是否在深浅主题下可辨。
字体放大需要独立一轮。固定断点只描述默认字体布局,系统大字号会增加按钮和状态文本宽度。测试不应通过关闭字体放大来获得通过结论,而应记录何处开始省略、是否仍有无障碍名称、危险动作是否保持完整。若 640 vp 与大字号无法同时容纳四个冲突按钮,可以继续增加冲突条高度或提供系统菜单,但不能把按钮缩成无法辨认的文字。
最终报告至少包含每档一张应用内部图、一段连续操作结果和一个横向溢出结论。自动化可通过组件几何或截图像素辅助发现重叠,人工仍需检查文本可读、焦点清晰和拖动手感。证据文件应和 Debug HAP 哈希绑定,避免后续包更新后继续引用旧窗口截图。
如果某一档失败,报告应写出首个不可完成任务和最小复现尺寸,不用"整体适配较好"一类模糊结论。断点修正后从相邻两档重新回归,防止解决 640 vp 的同时破坏 900 vp 停靠布局。
结语
自由窗口适配不是让界面"尽量塞进去",而是给每类信息建立明确预算。OhMarkdown 在窄窗口保留编辑、标签、保存和操作状态,让侧栏覆盖而非消失,让冲突动作换行而非缩小,让次要元数据在明确断点收起。宽窗口则恢复停靠侧栏和完整状态,不需要重启或重新加载文档。
提交 b11519c 已将代码和自动化推送仓库。设备恢复后要从 640 vp 开始逐档验证,并特别观察 720、760、900 三个边界的状态连续性。只有窗口缩放过程、键鼠焦点和真实系统分屏都通过,鸿蒙 PC 优先才不只是设备类型声明,而是用户可以长期依赖的桌面工作方式。
用显示服务校准 vp,而不是给截图猜尺寸
模拟器屏幕物理分辨率为 3120 x 2080,DisplayManagerService 报告 Density: 1.9。最初按图片宽度估算窗口档位会把 1240 px 误写成 760 vp,实际上它只有约 653 vp 外框,正好接近 640 vp 内容最小约束。最终报告同时使用 DisplayManagerService 和 WindowManagerService:
text
Density: 1.9
720 vp -> 1368 px
760 vp -> 1444 px
900 vp -> 1710 px
1280 vp -> 2432 px
这一校准让断点结论可复现。720 vp 时状态栏重新显示 Markdown、UTF-8 和 LF,但分栏同步开关仍隐藏;760 vp 时同步开关出现;900 vp 时侧栏从覆盖转为停靠;1280 vp 时五个标签、设置面板、源码、预览、同步开关和完整状态栏同时显示。

640 vp 内容约束下,活动栏保持可用,设置侧栏覆盖编辑工作区而不是继续压缩核心正文,状态栏只保留操作状态和字数。截图看起来侧栏占比很大,这是明确的临时覆盖语义;关闭侧栏后编辑器仍拥有完整窗口宽度,正文和标签会话没有重建。

1280 vp 截图验证的不是"更宽更好看",而是宽度恢复后信息完整且状态连续:同一图片链接、同一 PRD 正文、同一分栏位置、同一设置选项都保留。整个缩放序列没有重新加载 ArkWeb,没有清除脏标记,也没有让预览迟到结果覆盖当前文档。
自由窗口小阶段由 2 分提升到模拟器 3 分。仍未验证 1440 vp、系统字体放大、不同密度显示器、系统分屏吸附和鸿蒙 PC 真机,所以不会给出 4 分。后续窗口回归必须继续记录物理宽度、密度、换算 vp 和 WindowManagerService 几何,避免证据再次因缩放比例而失真。