Repository navigation
Pin And Compositing
pin/ 持有贴图内容、窗口尺寸、缩放/透明度/锁定状态和清理逻辑。图片数据由内存所有权管理,不生成临时文件。窗口销毁时通过窗口事件自动从注册表移除条目。
生命周期:
clip / 图片 / 截图
→ PinManager
→ 隐藏创建窗口
→ 首帧就绪
→ show() → set_focus() → 摆位置顶
→ 缩放 / 透明度 / 锁定 / 复制 / 保存
→ 销毁清理
origins.rs 的 PinOriginRegistry 记住「我们自己截下来复制走的图」原本在屏幕上的矩形,之后从历史里贴同一张图时靠它贴回原处。
键是解码后像素的 sha256(含宽高),不是 PNG 字节。 图片经剪贴板走一圈是原始 RGBA,watcher 会重新编码,PNG 字节不稳定。
登记方交出的是 PinFingerprint(已算好的摘要 + 宽高),这样调用方能和剪贴板写入共用同一份解码像素,不必为登记再解一次或复制一份十几 MB。lookup 先只读 PNG 头比宽高,尺寸不匹配就直接返回,省掉整张解码。
这是一个容易出错的地方:
- 有原始矩形时直接用它(选区本来就是逻辑像素)
- 没有时入参是图片像素,必须先按合成器报告的真实缩放折成 CSS 像素
用整数缓冲区缩放或干脆不折算,都会让贴图被放大再重采样。详见显示缩放。
窗口外框有 252 px 高度下限(竖排工具条需要 249 px)。随手框一个小按钮的贴图按内容算只有一百多像素高,工具条会被切掉。
下限只加高窗口、不改内容尺寸,多出来的高度由前端留成左上角对齐的透明留白——否则「贴回原处」当场就偏。
只有位置真的可信才重新摆位。 Wayland 协议不把窗口位置告诉客户端,outer_position() 与 Moved 都是 GTK 自己那份假值。拿它算「保持中心」等于每格滚轮都把窗口传送到工作区原点,所以 known_pin_position 在 Wayland 上返回 None。
GNOME Wayland 上 set_position 与 set_always_on_top 都是静默空操作。摆位和置顶只能借道 Shell 扩展,因此顺序必须是:
show() → set_focus() → PlaceWindow
因为窗口对象要映射后才存在。缩放之后还要再摆一次(改尺寸会把窗口带回普通层)。尺寸始终留在客户端。
两条路都失败只是位置/层级不理想,绝不让贴图本身失败。
PlaceWindow 是缩放的每帧热路径,它的前置检查(会话类型、扩展装没装、协议版本、令牌文件内容)结果被缓存:成功的结果一直有效,失败的结果 30 秒后重试,安装/卸载立刻作废缓存。
缩放与不透明度是每帧都会走的路径,所以 update_pin 的应答是 PinState(label、内容尺寸、scale、opacity、locked、position),不带图片字节或文本。
每帧重编一张全图 base64 纯属浪费,而且会让前端重建 object URL、造成闪烁。前端 mergePinState 把应答合并进手里那份 payload。
同一时刻只允许一个 update_pin 请求在飞,在飞就攒着——避免每帧向 GTK 主线程堆同步 IPC。
pin/project.rs 是可编辑 PNG 的信任边界。
标准 PNG 的 IDAT = 最新合成图(任何看图软件都能打开)
clippy-project iTXt = 压缩的自包含工程数据
├── canonical 原图
├── 尺寸 / sha256
├── annotations
├── adjustments
├── rendererVersion
└── preview.rgbaSha256
preview.rgbaSha256 以「宽高 + 解码 RGBA」绑定当前 IDAT。这样做的效果:
- 拒绝把合法工程块移植到另一张图上
- 允许同像素 PNG 改变压缩级别或 chunk 切分
容器 v2 仍可读,并在下一次可编辑保存时升级为 v3。renderer v1/v2 都能恢复,未知版本安全降级成普通 PNG。
writer 与 reader 共用:64 MiB 合成图、64 MiB 原图、96 MiB 解压 JSON、160 MiB 容器。
损坏的元数据或摘要不匹配只丢可编辑性,不让整个文件不可用。
- editable 保存失败不得静默伪装成 flat。 用户以为存的是可继续编辑的工程,实际存成扁平图,下次打开发现标注全没了。
- flat 导出会重编码并移除工程块。 否则模糊、马赛克处理过的文件里仍然内嵌着原图,等于泄露。
主窗口常态不提供普通图片打开按钮。把单个 PNG 拖入时,open_pin_project_file 只为通过全部校验的工程创建贴图;普通、损坏或未来版本的工程返回 None,不绕过剪贴板队列。
pin/render_v2.rs 是贴图工程与截图选区的唯一最终合成器。
Copy、扁平保存、可编辑保存、截图的 Copy/Save/Pin 复用同一语义。调整、四种效果、九种矢量/文字工具全部走锁定版本的纯 Rust CPU 路径和内嵌 Noto 字体,不查询 WebView、GPU 或系统字体。
因此相同输入可以用 RGBA SHA-256 金图锁定。
| 预算 | 值 |
|---|---|
| 渲染上限 | 32 Mi 像素 |
| 模糊缓存 | 32 Mi 像素 |
| 效果工作量 | 512 Mi 像素 |
任务在 blocking worker 执行。v1 未修改时复用 IDAT,第一次真实编辑才升级 v2。
画布文档的唯一坐标系是 canonical source pixels。前端提交 renderer v2 操作层,不上传一份互相竞争的 WebView PNG。
projectSchema.ts 在 IPC 之后再次防御性校验持久化文档;usePinCanvas.ts 恢复 annotations/adjustments/history baseline,并用 revision/savedRevision 判断 dirty。
关闭工具只停止交互,已有 composition 仍覆盖显示。editable save、flat export 与最新合成复制是三个显式动作,但共用后端的权威合成结果。
工具核心在 react/annotation/,与窗口无关;贴图和截图覆盖层都只提交操作文档。共 16 个工具分三组:
| 分组 | 工具 | 说明 |
|---|---|---|
| 选择 | select、object、eraser | select 就是框选/移动/缩放选区(选区自己是裁剪框,因此没有独立 crop 工具);橡皮一次点击删一个标注,保持撤销粒度 |
| 绘制 | pen、marker、rect、ellipse、line、arrow、measure、text | 四种拖拽形态(折线/矩形/线段/文本)复用同一套包围盒、命中与移动逻辑;ellipse 只在轮廓附近命中;measure 标注原图像素长度 |
| 效果 | highlight、blur、mosaic、spotlight、magnifier | blur/mosaic/spotlight/magnifier 需读取或压暗底图,始终先于矢量标注绘制;magnifier 从原图重采样使预览与导出清晰度一致 |
highlight 只是半透明矢量色块,按用途归在效果组,但绘制顺序仍跟随矢量标注。
马赛克必须以原图像素采样,不能采样已经绘制过的画布——否则重复涂抹会逐次变糊,且预览与导出不一致。
viewer/ 是独立窗口,按不可变图片快照隔离操作:原历史条目被删除不影响已打开的图片。
- 无限平移画布,「适应图片」可找回移出视口的图
- 自绘顶栏(拖动、最小化、全屏切换、关闭),不用系统标题栏
- 同一张图聚焦已有窗口,不同图独立开窗;最小化后再次点击同图恢复原窗口及编辑内容
- 绘制沿用同一套画布文档;复制、保存和贴图使用后端原图合成
窗口控制命令需要核验 caller label 与句柄;重复打开按 show → unminimize → focus 顺序恢复。
全屏切换要等权威状态,不能乐观假设:外部窗口管理器切换后,Esc 必须等到确认状态才动作,查询失败或等待期间出现保存模态时不能晚一步关掉窗口。