鸿蒙 PC Markdown 编辑器可调侧栏:拖动、键盘微调与宽度持久化

📅 2026/7/22 3:30:31
鸿蒙 PC Markdown 编辑器可调侧栏:拖动、键盘微调与宽度持久化
鸿蒙 PC Markdown 编辑器可调侧栏拖动、键盘微调与宽度持久化固定侧栏在移动端常常可以接受在 PC 编辑器中却会迅速暴露局限。文件名较长时 264 vp 不够用户需要展开目录和搜索上下文专注写作时又希望尽量把空间让给正文。用户指出 OhMarkdown 文件面板与编辑区之间的边界应当可以自由滑动修复目标因此不是“加一条可拖线”而是建立包含命中区域、光标、范围、窗口约束、键盘、无障碍和重启恢复的完整桌面交互。实现位于公开仓库 https://gitcode.com/VON-/codex_md_oh功能提交为358eb3f窄侧栏搜索布局收口提交为0d8d38b。本文使用当前真实代码和 HarmonyOS MateBook Pro 2in1 模拟器证据描述 220 至 480 vp 可调范围、至少 520 vp 编辑区预算、16 vp 键盘步长、Preferences 持久化以及拖动取消的回滚语义。窗口小于 900 vp 时仍沿用自动收起侧栏策略文章不会把窄窗口折叠误写为连续缩放。从用户任务定义尺寸而不是先写手势用户调节侧栏有三类任务。浏览深层目录时需要看清相对路径工作区搜索时需要阅读匹配摘要写作时希望缩窄面板。成功标准包括拖动过程中内容连续联动不重载编辑器到达边界时稳定停止重新启动保持上次宽度窗口变窄时不能挤掉核心编辑区键盘和辅助技术也能操作。失败后果不只是“不够漂亮”。如果侧栏没有最大值用户能把编辑区压到不可输入若每帧写 Preferences拖动会产生大量持久化 I/O若取消手势仍保存中间值界面状态不可预测若命中区只有 1 vp鼠标很难抓住若只支持鼠标键盘用户无法调整。因此数据模型、约束和生命周期要先于视觉。OhMarkdown 把宽度作为 WorkspaceShell 的原生状态所有上下文面板共用分隔线只修改这一个事实来源文件、搜索、大纲、设置同时获得一致宽度。宽度常量表达产品边界SettingsService.ets定义默认、最小和最大值。默认 264 vp 延续既有布局220 vp 是最窄仍可操作的面板480 vp 让长路径和搜索摘要获得空间但不会在常用窗口里过度占用。exportconstDEFAULT_SIDEBAR_WIDTH:number264;exportconstMIN_SIDEBAR_WIDTH:number220;exportconstMAX_SIDEBAR_WIDTH:number480;exportfunctionparseSidebarWidth(value:preferences.ValueType):number{if(typeofvalue!number||!Number.isFinite(value)){returnDEFAULT_SIDEBAR_WIDTH;}returnMath.max(MIN_SIDEBAR_WIDTH,Math.min(MAX_SIDEBAR_WIDTH,value));}解析函数同时服务读取和写入。Preferences 可能来自旧版本、调试工具或异常中断不能假设永远是合法有限数字。字符串、NaN、Infinity 回退默认有限数值钳制到产品范围。这样损坏设置不会让 ArkUI 收到非法尺寸。范围使用 vp 而不是物理像素使不同显示密度下交互尺度保持一致。它不是按窗口百分比保存因为用户在不同窗口大小间切换时通常希望记住“面板大约多宽”而不是让 25% 在超宽屏上变成巨型侧栏。窗口预算需要第二层动态约束静态最大 480 vp 仍不能适用于所有窗口。工作台还有 48 vp 活动栏、8 vp 调整命中区编辑器必须至少保留 520 vp。因此 WorkspaceShell 根据当前窗口宽度计算动态 maximum再进行二次钳制。privateclampSidebarWidth(width:number,windowWidth:numberthis.windowWidth):number{constavailableWidthwindowWidth-ACTIVITY_RAIL_WIDTH-SIDEBAR_RESIZE_HANDLE_WIDTH-MIN_EDITOR_WORKSPACE_WIDTH;constmaximumWidthMath.max(MIN_SIDEBAR_WIDTH,Math.min(MAX_SIDEBAR_WIDTH,availableWidth));returnMath.max(MIN_SIDEBAR_WIDTH,Math.min(maximumWidth,width));}外层Math.max(MIN_SIDEBAR_WIDTH, ...)保证动态最大值不会低于最小侧栏。不过窗口小于 900 vp 时shouldShowSidebar会直接返回 false活动面板收起把空间交给编辑器所以极窄窗口不会真的同时强塞 220 vp 侧栏和 520 vp 编辑区。onAreaChange每次获得新窗口宽度后重新钳制当前值。用户在大窗口保存 480 vp随后把窗口缩窄侧栏会自动收紧窗口再放大时当前会保持收紧值而不是突然跳回 480。是否恢复用户偏好可在未来增加“期望宽度”和“有效宽度”双状态当前优先保证布局稳定。命中区与视觉线应当分开人眼需要的是细分隔线鼠标需要的是更宽命中区域。OhMarkdown 使用 8 vp 的 Stack 作为交互面内部居中绘制 1 vp Divider悬停或拖动时线宽和颜色变化。这样界面仍克制但用户无需精确点中单像素。BuilderprivatesidebarResizeHandle(){Stack(){Divider().vertical(true).height(100%).strokeWidth(this.sidebarResizeActive?2:1).color(this.sidebarResizeHovered||this.sidebarResizeActive?#087A63:$r(app.color.workspace_border))}.width(SIDEBAR_RESIZE_HANDLE_WIDTH).height(100%).backgroundColor(this.sidebarResizeActive?$r(app.color.workspace_sync_selected):Color.Transparent)}稳定 8 vp 宽度还有布局价值悬停和拖动只改变颜色、线宽不改变 Row 的轨道尺寸编辑器不会因 hover 左右抖动。分隔线本身不是装饰卡片而是工作区结构控件贯穿内容高度且不占用顶部标签栏之外的额外空间。截图中用户用红框标出的正是这一边界。实现后视觉仍接近原布局但鼠标移入会变成横向调整光标按下后侧栏与 Web 区域连续移动。PanGesture 的累计偏移模型拖动开始时记录sidebarResizeStartWidth更新阶段使用“起始宽度 本次手势累计 offsetX”。不要在每个 update 中用“当前宽度 累计 offsetX”否则偏移会被重复叠加侧栏加速跳动。.gesture(PanGesture({fingers:1,direction:PanDirection.Horizontal,distance:1}).onActionStart((){this.sidebarResizeStartWidththis.sidebarWidth;this.sidebarResizeActivetrue;cursorControl.setCursor(pointer.PointerStyle.WEST_EAST);}).onActionUpdate((event:GestureEvent){this.sidebarWidththis.clampSidebarWidth(this.sidebarResizeStartWidthevent.offsetX);}).onActionEnd((){this.sidebarResizeActivefalse;this.persistSidebarWidth();}).onActionCancel((){this.sidebarWidththis.sidebarResizeStartWidth;this.sidebarResizeActivefalse;}))distance: 1让 PC 鼠标很快进入拖动不要求像触控滚动那样越过较大阈值。方向限制为 Horizontal垂直移动不会被解释成尺寸变化。每个 update 都通过动态 clamp因此越界时宽度停住不会先越过再在结束时弹回。取消手势恢复起始宽度这与结束语义明确区分。系统打断、手势竞争或指针取消不能留下半完成值。结束时才持久化拖动几十帧只更新内存与布局不产生几十次 Preferences flush。指针光标与清理生命周期悬停进入命中区时设置WEST_EAST离开且没有拖动时恢复默认。拖动开始再次设置光标避免指针轻微移出命中区后样式丢失。组件离开页面时若仍处于 hover 或 activeaboutToDisappear主动恢复默认防止全局光标残留到其他页面。.onHover((isHover:boolean){this.sidebarResizeHoveredisHover;if(isHover){cursorControl.setCursor(pointer.PointerStyle.WEST_EAST);}elseif(!this.sidebarResizeActive){cursorControl.restoreDefault();}})光标是桌面可发现性的关键。仅靠一条浅灰线用户未必知道可以拖横向双箭头是成熟 PC 约定。拖动中的绿色反馈进一步说明当前操作有效但颜色不是唯一提示键盘焦点和辅助功能文本同样存在。全局光标 API 要特别注意异常清理。当前onActionCancel只恢复宽度和 active光标最终由 hover 离开或页面消失恢复后续可在取消路径直接检查 hover 并恢复使状态更严密。现有模拟器路径没有出现残留但这是可继续收口的实现细节。键盘微调不是附加功能分隔线设置focusable(true)和tabStop(true)左右方向键每次改变 16 vp。步长足够明显又比鼠标拖动更可控更新同样经过 clamp随后立即持久化。privatehandleSidebarResizeKey(event:KeyEvent):boolean{if(event.type!KeyType.Down||(event.keyCode!KeyCode.KEYCODE_DPAD_LEFTevent.keyCode!KeyCode.KEYCODE_DPAD_RIGHT)){returnfalse;}constdeltaevent.keyCodeKeyCode.KEYCODE_DPAD_LEFT?-16:16;this.sidebarWidththis.clampSidebarWidth(this.sidebarWidthdelta);this.persistSidebarWidth();returntrue;}只消费 Key Down避免 Key Up 再调整一次。其他按键返回 false继续由正常焦点系统处理。达到边界后再次按键宽度不变但事件仍被消费避免方向键意外滚动相邻区域。键盘路径也是精确测试边界的一种方式从 264 连续左移可以验证最小 220从 392 右移验证最大 480。当前设备主要对拖动和辅助功能树做了闭环键盘处理由代码和构建覆盖后续真机测试应记录焦点环可见性与连续按键手感。Slider 语义与辅助功能描述调整柄的业务角色更接近 Slider 而不是普通 Button。实现设置AccessibilityRoleType.SLIDER名称取资源“调整侧栏宽度”description 实时暴露${Math.round(sidebarWidth)} vp。.focusable(true).tabStop(true).accessibilityText($r(app.string.resize_sidebar)).accessibilityDescription(${Math.round(this.sidebarWidth)}vp).accessibilityRole(AccessibilityRoleType.SLIDER).onKeyEvent((event:KeyEvent):booleanthis.handleSidebarResizeKey(event))模拟器强制重启后辅助功能树返回Stack ... description392 vp同时 Web 边界从 x1374 开始。这把抽象设置值与实际布局联系起来比单看截图更能证明持久化生效。当前描述没有分别暴露最小值、最大值和标准化当前值字段依赖文字 description后续若 ArkUI 提供更完整范围语义应补充。现有实现至少保证屏幕阅读器知道控件用途和当前尺寸并能通过方向键改变。所有上下文面板共享一个宽度文件、搜索、大纲、设置面板都使用.width(this.sidebarWidth)。活动栏切换只改变面板内容不改变侧栏轨道。用户为长文件名调宽后切到全文搜索仍得到相同空间为写作调窄后设置面板也保持窄宽不产生视觉跳动。BuilderprivatecontextualPanel(){if(this.activePanelfiles){this.filesPanel();}elseif(this.activePanelsearch){this.searchPanel();}elseif(this.activePaneloutline){this.outlinePanel();}elseif(this.activePanelsettings){this.exportPanel();}}每个具体面板内部也绑定同一宽度。这里存在一定重复但它与现有 Builder 结构一致没有为一个状态引入新的抽象层。关键是没有面板保留硬编码 264测试才不会出现“文件可调、搜索固定”的局部实现。面板内部必须响应最小宽度。语言分段按钮使用布局权重长文件名省略搜索选项在0d8d38b增加 300 vp 断点。可调容器上线后检查所有子内容是 PC 自由窗口开发不可省略的第二阶段。Preferences 只在稳定时机写入宽度与语言、自动保存、图片目录共用ohmarkdown-settings。保存函数再次调用parseSidebarWidth即便调用方遗漏 clamp持久层也不会写入越界值。flush确保强制停止进程前落盘。exportasyncfunctionsaveSidebarWidth(context:Context,width:number):Promisevoid{constsettingsawaitpreferences.getPreferences(context,ohmarkdown-settings);awaitsettings.put(sidebar-width,parseSidebarWidth(width));awaitsettings.flush();}启动初始化异步读取成功后还要按当前窗口宽度动态 clamp失败则使用默认宽度。这样在大显示器保存 480随后用较小窗口启动时不会盲目恢复到挤压编辑器的值。拖动结束写一次键盘每次明确步进写一次。前者避免高频 I/O后者符合每次按键都是完成操作的语义。保存失败不会回滚当前视觉宽度只在状态栏按当前语言显示“无法保存侧栏宽度”当前会话可继续使用但重启可能恢复旧值。与编辑器 Webview 的联动WorkspaceShell 根布局是 Row活动栏、条件侧栏、调整柄、编辑工作区。侧栏宽度变化时ArkUI 重新分配剩余空间给editorWorkspaceWebview 自适应父容器。没有调用 Web 的 setDocument 或 reload文档、光标和状态保持。Row(){this.activityRail();if(this.shouldShowSidebar()){this.contextualPanel();this.sidebarResizeHandle();}this.editorWorkspace();}.onAreaChange((_oldArea:Area,newArea:Area){constwidthnewArea.widthasnumber;this.windowWidthwidth;this.sidebarWidththis.clampSidebarWidth(this.sidebarWidth,width);})模拟器从约 261 vp 拖到 392 vp 时Web 起点由横坐标 1125 移到 1374编辑器内容没有重载。继续向右停在 480向左停在 220。这个结果证明布局是连续分配而不是拖动结束后整体重建。CodeMirror 会响应 Webview 尺寸变化完成自身布局。当前没有在每帧手动调用 JavaScript resize避免 Bridge 消息风暴。浏览器布局机制足以处理父容器变化应用只控制原生轨道。真实鸿蒙 PC 模拟器证据下面截图来自 MateBook Pro 2in1 模拟器。侧栏由默认约 264 vp 拖动到 392 vp文件/空状态区域变宽编辑区同步向右移动分隔线保持清晰但不过度突出。设备继续验证上下限向右拖动停在 480 vp向左停在 220 vp。恢复到 392 后强制停止并重启辅助功能树再次显示description392 vpWeb 边界为[1374,431][2605,1626]。这证明宽度状态、实际区域和 Preferences 三者一致。截图中的系统原生标题栏空白仍用于窗口拖动不属于侧栏。应用内部顶部标签空白另由Alignment.Start修复。把三个不同空间问题分开才能避免用调整侧栏去掩盖标签对齐错误。搜索面板暴露的二次回归侧栏能缩到 220 vp 后中文“区分大小写、全词匹配、正则表达式”无法同时放在一行最右文字被裁切。原固定侧栏下不明显可调功能把隐藏假设暴露出来。最终以 300 vp 为断点窄侧栏两行宽侧栏单行。这个回归说明容器能力不能孤立验收。文件列表、搜索、设置、冲突提示、空状态都要在 220、300、408、480 vp 检查。开发可调侧栏时子组件必须把文字完整和稳定高度作为硬条件。0d8d38b修复后220 vp 辅助功能树中三个中文标签都有完整边界408 vp 三项纵坐标一致恢复单行。选项状态由 WorkspaceShell 保存布局分支只改变容器不重建语义状态。自动化、构建和设备测试最终 Playwright30/30通过证明 Web 编辑功能没有因容器变化回归。ArkTSUnitTestBuild新增合法宽度、默认回退、上下限钳制断言。Debug HAP 与 ohosTest HAP 构建通过MateBook Pro 2in1 模拟器 ohosTest7/7资源键集合一致git diff --check通过。最终 Debug HAP 大小 1,520,352 字节SHA-256367ab8650479aa1fa8fe73bd1ebadd9a53f46659c850c2e388fc799d5cb88e5bohosTest HAP 大小 2,360,824 字节SHA-256b7230037b51044fe16168d2c835fb891e1c70f675941a1046165bc895217592c。两份均为未签名测试产物。单元测试证明数值函数模拟器手势证明真实交互辅助功能树证明语义与持久化截图证明视觉结果。当前尚未记录真机不同显示缩放下的拖动手感也没有长期用户偏好数据不应从模拟器结果推导出所有硬件已完成验证。已知限制与演进方向窗口缩小时当前会把有效宽度直接收紧之后放大不自动恢复之前更大的用户期望。可增加preferredSidebarWidth与effectiveSidebarWidth双值Preferences 保存前者布局根据窗口得到后者。这样窗口恢复时可以恢复用户选择但状态复杂度和测试矩阵会增加。双击分隔线恢复默认、右键预设尺寸、折叠按钮和触控更大命中区尚未实现。当前 8 vp 对鼠标足够触控设备是否需要动态 12 或 16 vp 要用真机测量。键盘步长固定 16 vp暂不支持 Home/End 直接到边界。辅助功能目前以 Slider 角色和 description 暴露当前值后续应评估平台是否支持标准 range value。持久化写入失败只有状态栏提示没有重试队列设置规模很小当前可接受但若 Preferences 成为统一配置中心应增加结构化错误与诊断。结论OhMarkdown 的可调侧栏从一个视觉分隔线扩展成完整 PC 控件220 至 480 vp 静态范围、至少 520 vp 编辑区预算、8 vp 稳定命中区、横向光标、累计 PanGesture、取消回滚、16 vp 键盘步进、Slider 无障碍语义和 Preferences 重启恢复。358eb3f建立能力0d8d38b修复最小宽度下的搜索子布局。真实模拟器把侧栏调到 392 vp 并在重启后恢复Web 内容没有重载全套自动化、构建和设备测试继续通过。这类细节正是鸿蒙 PC Markdown 编辑器与简单移动端放大版的区别用户能够按任务分配工作空间同时产品仍守住编辑区、文档状态和可恢复设置的边界。