鸿蒙 PC Markdown 编辑器标签栏对齐:消除单标签留白而不破坏水平滚动
鸿蒙 PC Markdown 编辑器标签栏对齐:消除单标签留白而不破坏水平滚动
用户截图中,Untitled.md左边出现了一大段空白,看起来像编辑器布局没有填满。问题并不在窗口最上方的系统标题栏,也不在文件侧栏宽度,而在应用内部标签滚动容器的内容对齐:只有一个短标签时,横向 Scroll 的子 Row 位于剩余轨道中部;标签增多后又不容易察觉。PC 编辑器的首个标签应紧贴编辑工作区左边界,空白只能出现在所有标签之后。
修复已进入公开仓库 https://gitcode.com/VON-/codex_md_oh,实现提交为358eb3f,窄侧栏联动修复后的验证基线为0d8d38b。本文从布局树、默认对齐、滚动约束、固定标签尺寸、系统标题栏边界、焦点和设备验证解释为什么一行.align(Alignment.Start)能修复现象,但完整交付仍需要多标签、窄窗口、侧栏调整和自动化回归。
先把三种空白区域分清
截图里至少有三种“看起来为空”的区域。窗口最上方、应用名称右侧是 HarmonyOS 原生标题栏的拖动空间,用户在这里拖动窗口,不能由应用标签填满;左侧上下文面板为空时是文件树内容区,承担打开文件夹的空状态;应用标签栏中首标签左侧的空白没有任务价值,才是需要修复的部分。
如果误把系统标题栏当成 bug,应用可能尝试覆盖系统拖动区域,破坏窗口移动、最大化和系统按钮;如果误把标签空白归因于固定侧栏,拖动侧栏只能改变空白长度,不能让标签回到起点。定位布局问题的第一步,是用组件边界和层级确认哪一块由谁拥有。
最终测试报告明确保留原生标题栏,修复下方应用标签轨道。设备截图中Untitled.md紧贴 Web 工作区左边界,系统顶部依然有合理拖动空间,两者不冲突。
标签栏的真实布局树
WorkspaceShell.tabBar外层是 Row。左侧横向 Scroll 使用layoutWeight(1)占据除右侧工具按钮外的剩余宽度;Scroll 内部是一个 Row,依次放文档标签和新建图标。右侧是打开、保存、源码/分栏/预览等固定工具。
@BuilderprivatetabBar(){Row(){Scroll(){Row(){ForEach(this.documentSessions,(session:DocumentSession)=>{this.documentTab(session)})this.iconButton($r('sys.symbol.plus'),$r('app.string.new_document'),()=>{this.requestEditorCommand('new');})}}.height('100%').layoutWeight(1).scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)Button($r('app.string.open_action'))Button($r('app.string.save_action'))}}Scroll 的 viewport 很宽,而单个标签和加号只占较小内容宽度。当内容短于 viewport 时,“内容应该放在轨道哪里”由 Scroll 对齐决定;当内容超过 viewport 时,才进入水平滚动。之前代码没有明确声明对齐,依赖平台默认值。
这类问题常在响应式布局中出现:开发者关注溢出后的滚动,却忘记非溢出状态也有对齐语义。默认值即使在某版本或某容器中看似合理,也不应替代产品明确要求。
默认居中为何只在单标签暴露
当前 ArkUI 环境下通用内容对齐表现为居中。假设滚动 viewport 可用宽度 700 vp,单标签 156 vp,加号 32 vp,内容合计约 188 vp,剩余 512 vp 会分布在内容两侧,首标签左边就出现约 256 vp 空白。用户看到的正是这种量级。
当打开四五个标签,内容宽度逐渐接近 viewport,居中空白缩小;超过 viewport 后滚动内容本身比视口更宽,起点问题也不再以大块空白呈现。因此依赖多文档日常测试很容易漏掉,而新建应用的默认单标签首屏最明显。
侧栏宽度也会改变 viewport。侧栏越窄,编辑工作区越宽,单标签剩余空间越大,错误空白反而更明显;把侧栏拖宽后空白缩短,可能让人误以为它与侧栏相关。真正稳定的修复必须直接指定 Scroll 内容起点。
用 Alignment.Start 声明产品意图
修复只在 Scroll 上增加.align(Alignment.Start):
Scroll(){Row(){ForEach(this.documentSessions,(session:DocumentSession)=>{this.documentTab(session)},(session:DocumentSession):string=>`${session.id}:${session.name}:${session.dirty?'dirty':'saved'}`)this.iconButton($r('sys.symbol.plus'),$r('app.string.new_document'),()=>{this.requestEditorCommand('new');})}}.height('100%').layoutWeight(1).align(Alignment.Start).scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)Start 表达的是逻辑起点而非硬编码像素偏移。当前中英文界面都采用从左到右布局,首标签从左边界开始。未来若产品支持 RTL,需要验证 ArkUI 的逻辑方向映射,而不是把这里改成绝对 Left。
这行代码没有给内部 Row 设置width('100%')。强行让内容 Row 填满视口后还要再决定 Row 内部排列,可能影响内容溢出测量;直接让 Scroll 对齐实际内容更符合组件职责。
水平滚动能力必须继续保留
修复目标是单标签左对齐,不是取消 Scroll。OhMarkdown 支持多文档会话,每个标签固定 156 vp,打开多个文件后总宽度会超过可用轨道。Scroll 仍设置 Horizontal,滚动条隐藏以保持紧凑桌面外观。
.align(Alignment.Start)只影响内容小于 viewport 时的位置,不会把溢出内容压缩到一行或禁用滚动。多标签顺序仍由documentSessions决定,点击激活、关闭和脏状态没有变化。加号跟在最后一个标签之后,单标签时位于其右侧,多标签时随内容滚动。
右侧打开、保存和模式按钮不在 Scroll 内,所以标签再多也不会把关键文件操作推走。layoutWeight(1)让滚动轨道吸收剩余空间,固定工具区保持可访问。这是 PC 工具栏比简单Row + ForEach更可靠的结构。
固定标签尺寸避免动态内容改轨道
每个文档标签宽度 156、高度占满标签栏,内部由文档图标、文件名、脏标记和关闭按钮组成。文件名maxLines(1)、layoutWeight(1)并使用 Ellipsis;脏标记预留 8 vp,即使保存状态变化也不会让关闭按钮横向跳动。
Row({space:7}){SymbolGlyph($r('sys.symbol.doc_plaintext'))Text(session.name).maxLines(1).layoutWeight(1).textOverflow({overflow:TextOverflow.Ellipsis})Text(session.dirty?'*':'').width(8)Button(){SymbolGlyph($r('sys.symbol.xmark'))}.width(22).height(22)}.width(156).height('100%')稳定尺寸让对齐和滚动行为可预测。若标签宽度随完整文件名变化,一个超长中文名称可能占满整条轨道,其他标签位置和滚动量大幅跳动。PC 编辑器更适合固定轨道加省略,完整路径可在文件树或后续 tooltip 中查看。
激活态使用背景和底部 2 vp 强调线,未激活态使用细右边框。状态改变不会改变标签外尺寸,因此.align(Alignment.Start)的起点不会因保存或激活发生位移。
标签身份与渲染键
ForEach 的键包含 session id、名称和 dirty 状态。session.id保证文档会话身份;名称和 dirty 变化触发对应标签视图更新。关闭操作按 id 路由,不依赖显示名称,两个同名文件也不会关闭错误会话。
ForEach(this.documentSessions,(session:DocumentSession)=>{this.documentTab(session)},(session:DocumentSession):string=>`${session.id}:${session.name}:${session.dirty?'dirty':'saved'}`)对齐修复没有改变数据模型或 key,降低回归面。它不应顺便重构多文档会话,因为用户反馈是明确的小范围布局问题。保持提交聚焦也使设备截图和测试结果更容易归因。
当前键包含可变字段意味着名称或脏状态变化时组件可能被重建,焦点是否受影响值得后续评估;更稳定的策略可以只用 session id,并依赖状态更新重绘。但该问题与首标签留白无直接关系,当前提交没有扩大修改范围。
系统标题栏为何必须保留空白
HarmonyOS PC 窗口最上方由系统提供应用图标、名称、窗口控制和可拖动标题区域。应用截图中 OhMarkdown 名称右侧到最小化按钮之间的空白承担移动窗口任务。桌面用户依赖这一区域拖动、双击最大化或使用系统窗口行为。
应用内部标签栏位于其下。它应从编辑工作区左边界开始,但不应侵入系统标题栏。两者视觉上都横向延伸,容易被红框一起圈中;从组件边界看,系统栏不在 WorkspaceShell 的 build Row 中,应用无法用Alignment.Start改变它。
修复报告明确说明“原生标题栏拖动空间按系统交互规范保留”。好的问题处理不仅消除真正 bug,也要说明为何相邻空白不能消除,避免后续再次以填满为目标破坏平台能力。
与可调侧栏的组合关系
WorkspaceShell 根 Row 依次是活动栏、侧栏、8 vp 调整柄和编辑工作区。标签栏属于editorWorkspace,所以其逻辑起点随侧栏拖动一起移动;在自己的坐标系中,首标签始终从 x=0 开始。
Row(){this.activityRail()if(this.shouldShowSidebar()){this.contextualPanel()this.sidebarResizeHandle()}this.editorWorkspace()}默认侧栏约 264 vp 时,标签紧邻编辑区边界;拖到 392 vp 后,整个编辑区向右移动,标签仍紧邻新边界。旧实现会在每个新 viewport 中重新居中,所以侧栏变化后空白长度也变化。Start 对齐使二者解耦:侧栏决定编辑区在哪里,标签栏决定内容从编辑区哪里开始。
窗口宽度变化同理。右侧工具区占用固定空间,Scroll viewport 伸缩,内容不足时仍左对齐,内容过多时滚动。布局规则不再依赖具体窗口或标签数量。
真实模拟器截图
下面截图来自 HarmonyOS MateBook Pro 2in1 模拟器。Untitled.md标签从编辑工作区左侧开始,加号紧随其后;旧截图中标签左侧的大段空白已经消失。左侧文件面板仍保持默认宽度,顶部原生标题栏拖动区仍存在。
设备记录显示默认布局中编辑器 Web 区域从横坐标 1125 开始。拖动侧栏到 392 vp 后 Web 起点变为 1374,标签继续贴合起点,文档状态没有重载。这个组合验证比单一静态截图更能说明对齐规则正确。
视觉检查还确认标签文字、关闭按钮、加号和右侧打开/保存/模式按钮没有重叠。截图为真实应用内部画面,不是设计稿,也没有用裁切掩盖系统标题栏。
窄窗口与侧栏折叠
根布局最小约束为 640 × 480,窗口小于 900 vp 时侧栏自动收起。侧栏消失后 editorWorkspace 向活动栏靠近,标签仍从其新左边界开始。Start 对齐不需要为有无侧栏写分支。
右侧工具按钮会减少 Scroll 可用空间,但横向滚动保护标签,不应通过缩小每个标签字体或动态缩放解决。显示文本必须保持专业可读,长名称省略,完整文档仍可通过文件树定位。
当前测试重点是常用 PC 窗口和可调侧栏,没有宣称所有极端最小窗口的工具栏按钮都已经完成折叠策略。若后续发现 640 vp 下右侧操作区过宽,应独立设计菜单收纳,而不是重新允许标签居中或覆盖按钮。
焦点、点击与无障碍
标签 Row 点击激活会话,关闭按钮有资源化 accessibilityText“关闭文档”,加号图标按钮使用“新建文档”。对齐变化不改变焦点顺序:滚动内容中的标签和加号仍先于右侧工具按钮。
激活文档由底部强调线、颜色和背景共同表达,不只靠位置。脏状态使用星号并预留宽度,当前还应进一步补充更明确的可访问描述,例如把“已修改”加入标签 accessibilityText;这属于后续无障碍增强,不是本次已完成能力。
鼠标点击关闭时事件是否冒泡到标签激活路径由 ArkUI Button 处理。修复未修改该交互。回归测试需要确认关闭非活动标签不会因对齐变更错误激活其他会话。
自动化与构建验证
布局提交后执行 Playwright30/30,证明 CodeMirror、多文档、命令、搜索和预览未受原生轨道变化影响。ArkTSUnitTestBuild、Debug HAP 与 ohosTest HAP 构建通过,MateBook Pro 2in1 模拟器 ohosTest7/7,中英文资源键一致,git diff --check通过。
最终 Debug HAP 大小 1,520,352 字节,SHA-256367ab8650479aa1fa8fe73bd1ebadd9a53f46659c850c2e388fc799d5cb88e5b;ohosTest HAP 大小 2,360,824 字节,SHA-256b7230037b51044fe16168d2c835fb891e1c70f675941a1046165bc895217592c。产物均未签名,只用于本轮可追溯测试。
标签对齐主要依赖设备视觉与布局边界,不适合只靠纯函数单元测试。未来可增加 ArkUI UI 自动化,读取首个标签与 Webview 左边界并断言差值在预期范围,减少截图人工判断。
回归矩阵与错误方案
至少要覆盖:一个未命名标签、多个短标签、一个超长中文文件名、同名不同 URI、多标签溢出、脏状态切换、关闭活动与非活动标签、侧栏 220/392/480、侧栏折叠、中英文、亮暗主题。每种状态都要确认首标签起点、右侧工具稳定和水平滚动可用。
不推荐通过给首标签负 margin 抵消空白,因为空白随 viewport 和内容宽度变化;不推荐给内部 Rowposition({ x: 0 }),绝对定位会破坏滚动测量;不推荐删除 Scroll,多标签会溢出;也不应把标签栏移动到系统标题栏,窗口拖动和系统控制会变复杂。
明确Alignment.Start是最小且语义正确的方案。它利用组件提供的布局能力,修复原因而非截图结果,同时保持现有滚动和会话架构。
性能与稳定性
对齐属性只影响布局计算,不增加状态、监听、定时器或 Bridge 调用。内容短于 viewport 时位置从居中变为起点;内容溢出时滚动机制不变。它不会读取文档、重绘预览或复制 CodeMirror 状态。
固定标签宽度和隐藏滚动条保持轨道稳定。侧栏拖动期间 viewport 连续变化,布局引擎重新计算标签位置,但标签数量上限在现有工作流中较小;没有手工逐标签测量。大文件模式与标签对齐无关。
小改动仍需全套验证,因为 WorkspaceShell 是共享外壳。提交保持单一目的,让失败可以快速定位。后续若优化标签虚拟化或可拖动排序,应保留“第一个可见标签从逻辑起点开始”的不变量。
已知限制与后续方向
当前标签固定 156 vp,不支持用户调整、拖动排序、固定标签、分组或溢出下拉列表。水平滚动条隐藏后,可发现性主要依赖触控板/滚轮;大量标签场景可以增加左右滚动按钮或最近标签菜单。
文件名省略尚无统一 tooltip,用户可能需要回到文件树查看完整路径。脏状态星号的屏幕阅读器语义可以增强。RTL 语言尚未支持,Alignment.Start在未来语言方向下需要设备验证。
这些限制不影响本次问题结论:单标签不应居中,多标签不应失去滚动,系统标题栏不应被应用填充。后续功能都应在这三个不变量上演进。
结论
OhMarkdown 标签留白的根因是横向 Scroll 未声明内容起点,在单标签且 viewport 较宽时表现为居中。358eb3f使用.align(Alignment.Start)修复真实原因,同时保留固定标签尺寸、水平滚动、右侧工具区和多文档会话。模拟器截图证明Untitled.md已贴合编辑工作区左边界,可调侧栏改变工作区位置时对齐仍稳定。
这次修改也建立了重要的鸿蒙 PC 判断方法:先区分系统标题栏、应用标签栏和侧栏各自的所有权,再用组件语义修复布局,不用像素补丁追截图。小小一段空白背后,连接的是窗口管理、滚动容器、多标签状态和桌面交互的一致性。
