ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

BongoCat 模型窗口右键拖动缩放:从 ADR-0057 看无重建实时 Resize 的设计与实现

BongoCat 模型窗口右键拖动缩放:从 ADR-0057 看无重建实时 Resize 的设计与实现 桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载导读本文以架构决策记录 ADR-0057模型窗口用右键拖动缩放 为骨架拆解 BongoCat 桌面宠物在 Windows 与 macOS 上实现按住右键拖动即缩放模型窗口这一交互的完整设计包括与右键上下文菜单的按键冲突消解、缩放值的位移映射与配置契约收敛、拖动起点的窗口几何反算以及不重建原生窗口与 GPU 资源、就地 resize的实时渲染路径。读完本文你将掌握这套交互在 bongocat-overlay crate 中的落点便携判定逻辑位于 resize_drag.rsWindows 侧由窗口过程 window_proc.rs 驱动macOS 侧由 localNSEventmonitorresize_monitor.rs驱动并理解它为什么能保证每次拖动结果都能被配置契约接受、不会累积舍入误差、也不会与 hover 隐藏功能相互打架。背景为什么窗口缩放曾经如此重在 ADR-0057 之前模型窗口的尺寸只有一个来源配置项overlay.scale_percent。而且改变这个值意味着重建整个窗口OverlaySessionOptions::requires_window_recreation把scale_percent列为重建条件该判定逻辑位于 session.rs 的tick中重建会重新创建原生窗口HWND / NSPanel与 GPU 资源GpuModel::prepare还会重新加载全部模型纹理。也就是说旧交互只有设置页滑块这一条路且每次调整都伴随一次肉眼可见的窗口重建与资源重载。此外两个平台此前都不存在任何窗口 resize 通路渲染尺寸在窗口创建时固化进 swapchain/RTV/mask 纹理Windows或CAMetalLayer的 drawable sizemacOScrates/bongocat-overlay/src/windows/与crates/bongocat-overlay/src/macos/里没有 resize 代码。更麻烦的是按键已经被占用右键当前被上下文菜单独占。Windows 拦截WM_CONTEXTMENU | WM_NCRBUTTONUP并转发成OverlayContextMenuRequestmacOS 用NSEventlocal monitor 监听RightMouseUp两者都交给应用的muda菜单。任何新的右键交互都必须先解决这个同一个按键、两种意图的冲突。旧版pre-refactor分支的src/pages/main/index.vue曾在主窗口实现过类似交互if (buttons ! 2 || !shiftKey) return // 右键 Shift const delta (movementX movementY) * 0.5 catStore.window.scale round(clamp(scale delta, 10, 500))它要求按住右键 Shift范围10–500并且直接改window.scale与设置页滑块同源。ADR-0057 的产品实现没有移植这个交互而是重新设计了一套更严格的方案。核心决策一用位移阈值区分拖动与菜单点击旧版要求按住 Shift是因为用mousemove无法区分单击右键与按住右键拖动。ADR-0057 用位移阈值直接解决同一个问题且不要求 Shift右键按下后指针位移欧氏距离超过3px才算缩放RESIZE_DRAG_THRESHOLD 3.0未越阈值的右键仍然是上下文菜单越阈值之后即使缩放没有实际变化这次右键也不再弹菜单。位移始终相对按下点测量因此缓慢拖动不会因为单帧位移很小而被误判成单击一次手抖也不会因为累计位移而被误判成拖动。这在 resize_drag.rs 的observe中有明确实现先算(dx * dx dy * dy).sqrt()是否超过阈值dragging一旦置位就再也不会回退测试 resize_drag.rs 专门验证了单帧位移小但累计越过阈值仍算拖动resize_drag.rs 验证了不动就永不 resize。// resize_drag.rs 中的阈值判定简化 let dx pointer.0 - self.origin.0; let dy pointer.1 - self.origin.1; if !self.dragging (dx * dx dy * dy).sqrt() RESIZE_DRAG_THRESHOLD { return None; // 还没越过阈值仍是潜在的一次右键单击 } self.dragging true;核心决策二位移→缩放的映射与配置契约收敛映射公式沿用旧版的系数但范围收敛到当前配置契约scale clamp(按下时的缩放 (dx dy) * 0.5, 25, 400)系数RESIZE_DRAG_PERCENT_PER_PIXEL 0.5每个指针像素对应 0.5% 缩放。向右下拖动放大、向左上拖动缩小向右上/左下拖动时两个分量相互抵消范围25–400%旧版是10–500%与配置契约完全一致——OverlayConfig::scale_percent、OverlaySettings::is_valid以及 session.rs 的validate_options中25..400的校验都是同一个边界。这样做的意义在于拖动产生的结果必须总能被配置接受否则一次拖动会产出一个存不进去的值设置页滑块也找不到可显示的值。要放宽范围需要同时改 schema、fixture、设置页滑块与校验属于另一次契约变更ADR 已明确说明。resize_drag.rs 的observe按requested.round().clamp(...)得到整数缩放再通过ResizeBase::dimensions计算窗口尺寸// resize_drag.rs尺寸由基准与缩放重新算出 pub(crate) fn dimensions(self, scale_percent: u16) - (u32, u32) { let factor f64::from(scale_percent) / 100.0; ( cover_window_dimension(self.width * factor), cover_window_dimension(self.height * factor), ) }注意尺寸是由基准与缩放重新算出而不是在旧尺寸上累加像素——因此连续拖动不会累积舍入误差。cover_window_dimension位于 dimensions.rs负责ceil与最小/最大窗口尺寸钳制。核心决策三拖动起点反算自窗口实际宽度拖动开始时按下时的缩放不是读配置里的overlay.scale_percent而是由窗口当前宽度反算scale clamp(round(width / base * 100), 25, 400)理由很关键窗口几何与配置并不总是同步。用户拖动过一次、显示器 DPI 变了、或者手工编辑过配置之后配置值与窗口实际大小会脱节。如果拿配置值当起点第一次指针移动就会把窗口跳到另一个尺寸反算让拖动始终从用户看到的大小继续。ResizeBase::scale_percent_for_width实现了反算resize_drag.rs并对结果round().clamp(25, 400)保证写回的是一个合法值。测试 resize_drag.rs 验证了反算的往返一致性350→100、700→200、175→50并验证了越界宽度1、100000会被钳到契约边界测试 resize_drag.rs 专门演示了窗口在 420px 而配置仍写 100%对应 350px时拖动从 420 开始、第一次移动得到 140%而不是从 350 跳变。而ResizeBase本身要求两个维度都是有限正数resize_drag.rs非法值直接None此时右键退化为普通菜单点击。基准尺寸与 DPI 处理100%的基准尺寸是default_overlay_window_dimensions(canvas)与窗口创建时使用的同一个值dimensions.rs。它由DEFAULT_OVERLAY_WINDOW_WIDTH与模型的CanvasInfo宽高比推导保证非方形模型不会被拉伸。单位问题由平台层处理拖动状态机本身不需要知道 DPIWindows拖动状态机工作在物理像素SetWindowPos的单位。resize_base_for_dpi把逻辑基准按窗口当前 DPI 换算成物理像素geometry.rs换算公式为(logical * dpi 48) / 96。窗口过程在begin_resize时用GetDpiForWindow(hwnd)取当前DPI 而非创建时的 DPI因为窗口移动到别的显示器后必须按那块屏幕的像素缩放见 window_proc.rs 的注释。macOSResizeBase直接以点为单位NSEvent::mouseLocation给出的也是屏幕坐标。核心决策四锚点与实时跟随——不重建窗口的就地 resize锚点左上角保持不动窗口左上角保持不动与旧版setSize的观感一致WindowsSetWindowPos(..., SWP_NOMOVE)只改尺寸不动位置macOSNSWindowframe 原点在左下角所以按顶边不动修正origin.y即top frame.origin.y frame.size.height再setFrame:display:设置origin (frame.origin.x, top - size.height)。这是 resize_monitor.rs 的apply_resize所做的事注释明确说明这样才能让右键拖动的手感等同于旧版setSize。实时跟随逐帧 resize不重建、不重载纹理拖动过程中窗口逐帧改尺寸渲染器就地resizeWindowsIDXGISwapChain1::ResizeBuffers 重建 RTV/staging/每个 mesh 的 mask target见 session.rs 的sync_window_size与 session.rs 的resizemacOSsetFrame:display: 重设 drawable size 重建 mask 纹理。与尺寸无关的资源——device、pipelines、composition graph、模型纹理、顶点/索引缓冲——全程保留。值得注意 Windows 端的一个细节在 session.rs 的resize中ResizeBuffers之后会立即绘制一帧再返回窗口循环否则 compositor 可能把刚 resize 的 HWND 显示成未初始化的透明帧注释原文Fill it before returning to the window loop so the compositor cannot show the resized HWND as a transparent frame。这正是 ADR 修订说明2026-09-24要解决的问题缩放写回配置不再重建 HWND/NSPanel统一改为在现有原生窗口上更新几何并在尺寸变化后立即填充新的 swap-chain/drawable。拖动状态在 tick 中的同步每次 frame tick 都会调用self.overlay.sync_window_size()session.rs让 swap chain 与 mask target 跟上拖动直接改掉的窗口尺寸然后再绘制。如果此时渲染器尺寸没变例如没有拖动resize会因尺寸相等而成为 no-opwindow.rs 中先比较self.bounds()? bounds。平台适配窗口过程 vs NSEvent monitorWindows的拖动状态存在OverlayWindowState.drag里由窗口过程 window_proc.rs 驱动WM_NCRBUTTONDOWN | WM_RBUTTONDOWN触发begin_resize取当前 DPI 换算基准、反算起点缩放、SetCapture(hwnd)捕获鼠标保证指针移出窗口后消息仍能送达WM_NCRBUTTONUP | WM_RBUTTONUP之前的 move 消息喂给drag.observe有变化就apply_resizeSetWindowPosSWP_NOMOVE松手时先取走 drag 状态再ReleaseCapture()——因为释放捕获会同步投递WM_CAPTURECHANGED它会清空 drag 状态顺序反了会把一次拖动变成菜单点击window_proc.rs越过阈值drag.finish()若有新值就经resize_sender上报未越阈值走context_menu_sender弹菜单WM_CAPTURECHANGED捕获被别的窗口抢走或系统取消直接清空 drag避免状态卡死。macOS的 overlay 没有可做命中测试的窗口NSPanel 无内容、不可交互所以 localNSEventmonitor 是唯一能感知右键拖动的方式resize_monitor.rs 的模块注释明确说明了这一点。它用NSEventMask::RightMouseDown | RightMouseDragged | RightMouseUp注册按windowNumber过滤本窗口的事件坐标轴翻转AppKit 屏幕坐标向上增长而拖动数学假设 y 轴向下为正旧版 webview 与 Windows 适配器都如此所以resize_pointer()把 y 取反一次resize_monitor.rs不用窗口自身坐标空间——拖动过程中 frame 一直在变同一屏幕位置会读出不同值monitor 看到进程内所有右键事件这也是它必须把菜单请求交回应用的原因monitor 与 tray 菜单不能同时认领同一次点击。幂等写回配置后的尺寸不再被乘第二次缩放写回配置后runtime snapshot 的变化会让 frame tick 走原地尺寸更新分支而非重建。该分支先判断窗口尺寸是否已经等于新缩放对应的尺寸bounds_match_scale用abs_diff 1的 1px 容差bounds.rs吸收物理/逻辑换算的取整误差。相等时不再按比例重算——否则拖动刚设好的尺寸会被乘第二次需要改变尺寸时只更新现有 HWND/NSPanel 与 swap-chain/drawable并在返回 frame loop 前立即绘制一帧不替换原生窗口。// session.rs 中 tick 的缩放更新分支结构示意 if next_options.scale_percent ! self.options.scale_percent { let bounds self.overlay.window.bounds()?; let bounds if self.bounds_match_scale(bounds, next_options.scale_percent) { bounds // 拖动已把窗口设成这个尺寸不再按比例重算 } else { bounds.rescale(self.options.scale_percent, next_options.scale_percent) }; self.overlay.resize(bounds)?; }bounds_match_scale在 session.rs 中按窗口当前 DPI 重新换算基准后再比较Windows 端macOS 端同理。而requires_window_recreation(next_options)仍保留给真正需要重建的选项如角半径变化缩放单独走了就地路径。核心决策五持久化——overlay 只提请求配置的写入方仍是应用拖动结束时最终缩放通过新的OverlayInteractionSinks::resize_sender报给应用Windows 在WM_RBUTTONUP分支try_send(OverlayResizeOutcome { scale_percent })macOS 在finish_resize同样try_send。应用侧提交SettingsCommand::SetOverlaySettings写回overlay.scale_percent窗口几何仍由既有的 placement 通路OverlayWindowPlacementDebouncer→OverlayWindowPlacementChanged落到window-state.json。overlay 只提出请求配置的写入方仍然是应用——这与 ADR 一贯的配置单一写入者原则一致。同时要注意ResizeDrag::finish的语义resize_drag.rs没有越过阈值菜单点击、或越过阈值但缩放没有实际变化时返回None——前者是菜单点击后者没有需要持久化的新值。测试 resize_drag.rs 验证了对角线位移相互抵消、缩放不变时不写回。边界行为click-through 与 hover 隐藏的互斥click-through穿透模式下窗口返回HTTRANSPARENTWindows见 window_proc.rs 的命中测试分支或ignoresMouseEventsmacOS收不到指针消息因此不可拖动、也没有右键菜单。这与既有的非穿透模式才可拖动一致不是本次引入的限制。与 hover 隐藏的互斥拖动进行期间hide_on_pointer_hover不再生效。该功能会把窗口淡出并让指针事件穿透恰好会中断窗口自己正在执行的拖动。两个平台都在计算 hover 状态时把拖动中当作指针不在窗口内Windows 在 session.rs 的update_hover_presentation中检查!self.overlay.window.is_resize_dragging()macOS 的 resize_monitor.rs 提供is_resize_dragging()判断drag状态是否存在。因此拖动期间窗口保持可见拖动结束后恢复原有的 hover 行为。便携性设计无 GPU 也可测试的判定内核判定、映射、钳制与尺寸计算全部落在 resize_drag.rs是纯计算模块不依赖 GPU 与原生窗口平台适配器只负责三件事读取指针、改原生窗口尺寸、把最终缩放报出去。这个分层带来了极高的可测试性——resize_drag.rs内的mod tests覆盖了测试点验证内容源码位置宽度反算往返350/700/175 → 100/200/50357 → 102取整防漂移resize_drag.rs越界钳制宽度 1 / 100000 分别钳到 25% / 400%同上漂移窗口不跳变420px 窗口从 420 起算不跳到配置的 350resize_drag.rs基准合法性零/负/NaN/Inf 维度拒绝resize_drag.rs不动不缩放原地右键永远不 resize、可弹菜单resize_drag.rs阈值相对按下点单帧小位移累计越过阈值仍算拖动resize_drag.rs放大/缩小方向右下放大 200%、左上缩小 50%、触底 25%88pxresize_drag.rs缩放钳制±5000px 拖动钳到 400% / 25%resize_drag.rs无变化不写回对角线抵消越过阈值但不弹菜单、不写回resize_drag.rs同位重复观察同一位置只应用一次尺寸resize_drag.rs非有限坐标忽略NaN / Inf 指针位置直接忽略resize_drag.rs极端宽高比超宽模型缩到 25% 时尺寸下限优先仍可被配置接受resize_drag.rs影响与权衡Consequences行为变化右键不再无条件弹菜单。按住右键移动超过3px开始缩放松手后不弹菜单原地按下松开仍是原来的菜单。行为变化缩放范围是25–400%旧版为10–500%。这是配置契约的范围放宽需要同时改 schema、fixture、设置页滑块与校验。性能权衡拖动期间每个 frame tick 都会做一次 GPU resize——Windows 重建 render target、staging 纹理与全部 mask targetmacOS 重建全部 mask 纹理。这些资源都很小但拖动时的帧率低于静止时是预期结果尤其在有 mask 的模型上。写回无重建拖动结束后的写回不再重建 overlay 窗口只让 runtime snapshot 与已完成的窗口几何对齐设置页直接改缩放时现有窗口也会原地调整尺寸并立即绘制新尺寸第一帧避免未初始化透明 back buffer/drawable 暴露。单一配置来源缩放值从此有两个来源设置页与右键拖动两者写入同一个overlay.scale_percent因此设置页滑块在拖动结束后会显示拖动结果。位置与尺寸分离scale_percent在config.json实际几何在window-state.json。若两者不一致例如手工编辑配置下一次拖动会以当前窗口宽度反推的缩放为起点并在结束时把两者对齐。未验证项与后续关注ADR 明确列出的未验证项截至记录日期两平台的实机拖动观感未经人眼核验Windows 的ResizeBuffers路径、macOS 的 drawable resize 与 mask 重建同样未经过真机验证当前代码已通过 Windows 构建检查但真实拖动、显示器/DPI 切换与 compositor 透明帧仍需目标硬件 smoke 测试。这些是理解本 ADR 时必须注意的边界文档描述的是已实现且通过构建检查的设计而非已经过全平台实机验收的最终交互。小结ADR-0057 展示了一个典型的与既有交互争抢同一按键的架构决策过程用欧氏位移阈值区分菜单与拖动、把结果钳制进配置契约保证可持久化、以窗口实际宽度反算起点避免跳变、以就地 resize 立即填充新帧取代窗口重建、以 1px 容差的bounds_match_scale保证幂等。整个交互的可计算部分被剥离成 resize_drag.rs 的纯函数模块平台差异收敛为读指针、改几何、报结果三个适配动作是便携性分层与可测试性设计的直接范例。若需进一步了解窗口几何/placement 通路可继续阅读 bounds.rs、placement.rs 与 dimensions.rs。赞分享桌面应用【免费下载链接】BongoCat BongoCat — A cross-platform interactive desktop pet that brings fun to your desktop!项目地址https://gitcode.com/gh_mirrors/bong/BongoCat点击查看免费下载相关推荐ADR-0068 设置窗口销毁与进程内导航记忆BongoCat GPUI 设置窗口生命周期设计ADR 0068 设置窗口销毁与进程内导航记忆BongoCat GPUI 设置窗口生命周期设计 output_article BongoCat 设置窗口生桌面应用BongoCat 键位图驱动绑定模型没有键位图时按键不产生任何动作的实现方案ADR-0042BongoCat 键位图驱动绑定模型没有键位图时按键不产生任何动作的实现方案ADR 0042 本文基于 BongoCat 开源仓库中的架构决策记录 ADR桌面应用BongoCat 开发构建不提供登录时启动基于构建环境门禁的 ADR-0051 设计与实现解析BongoCat 开发构建不提供登录时启动基于构建环境门禁的 ADR 0051 设计与实现解析 BongoCat 在 ADR 0051 中做出了一项关键决策桌面应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表