dotnvim 是很久之前开的坑了,当时是想做一个颜值在线的 Windows neovim 前端。但其实我很少单独使用 vim,用也多半是在别的 IDE 里面用 vim 插件,所以这个项目也没怎么更新过;而且新的 Windows Terminal 完善之后,大概 dotnvim 存在的意义又下降了一分。但今天又打开看了一下,发现颜值其实还是挺不错的,居然又燃起了我间歇性使用 vim 的欲望⚠顺便再写一遍文章回忆一下当时开发时遇到的坑🤔
没有可用的 C# neovim 客户端
虽然当时在 neovim Wiki 的 Related Projects 里面有列出一个 C# 客户端,但实际看过就会发现功能欠缺很多,几乎处于不可用的状态。所以只好自己再造一个轮子了。neovim 与前端的通信使用的是 message pack,是一种很容易理解的协议。整个客户端的实现也很简单,就是 neovim 说什么,我们照做就行了——比如光标移动到哪里、颜色设置成什么、显示什么字符之类的。
但是只看 neovim 的 文档 还是会不明所以,所以搞不懂的时候就只能去翻官方的 Python 版或者是 Qt 版的客户端代码了。
Acrylic Blur
一开始我是打算做成 UWP 的,毕竟亚克力效果只有 UWP 才支持。起初考虑 nvim 需要自己单独一个进程,担心 UWP 不支持,结果是我多虑了。但后来翻了一下 UWP 的权限问题,似乎保存文件到任意位置的唯一方法只有弹出一个文件选择框,这与 vim 用户们的习惯大相径庭。最后就还是决定做成一个 Win32 的程序。
这样一来,如何实现 Acrylic Blur 就是个问题了。不过好在当时已经有很多关于 Windows 未公开的 API SetWindowCompositionAttribute 的资料了,所以自己琢磨一下试试也不难。(画外音:Windows 的兼容性负担就是被你们这群人搞上去的😀
当时还有很多采用自绘的方案,但跟原生的亚克力效果还是无法做到一致的。至于如何让整个窗体全都被透明特效覆盖到,这就属于 DwmSetWindowAttribute 和 DwmExtendFrameIntoClientArea 的工作了,这两个 API Aero 的时候就有了,幸亏以前用过,上手速度++。
Winform 还是 WPF?
本来准备使用 WPF,还在 WPF 和 D3D 交互上搞了半天,用了这个看起来没人维护了的 WPFDXInterop,它提供了一个 D3DImage 控件,只要把渲染好的图贴上去就可以了。但是最关键的是用它搞不定透明,如果在 WPF 里通过 WinformHost 的嵌入一个 Win32 控件的话,透明效果又不对。
所以最后我选择直接就使用一个 Win32 的窗体,上面所有的内容全部自己用 D2D 画,反正主体界面也不复杂,顶多就是几个 Button、再加上一个简单的 BoxLayout。至于其他的对话框(比如设置),就直接使用 WPF 就可以了。
1px 的不和谐边框
由于 Windows 10 上窗口周围都有 1 px(设备无关像素) 的边框,当我们的亚克力背景透明度调的比较低时,这个 1px 的边框就会露出来。这个问题实在无解,最后我就暴力地用 vim 当前的背景色不透明地先把这 1px 画上,盖住系统的边框,至少颜色上与当前使用的 Color Scheme 能够统一。如果我们的背景没那么透明,这样的边框就不显眼了。
次像素抗锯齿
这是我一直都没有搞定的一个问题。由于我们窗口的背景是透明的,次像素抗锯齿没办法启用,只能使用灰度抗锯齿。但灰度抗锯齿在低分辨率屏幕上的效果并不是很好。
我还特意去看了一下 UWP 的抗锯齿效果,也不是次像素抗锯齿(放大之后没有明显的红绿蓝边界),但好像要比普通的灰度抗锯齿的效果要顺眼(难道是错觉?)。好在我用 Surface,灰度抗锯齿效果在高分辨率屏幕上还不错,眼不见心不烦😞
连字 Ligature
怎么在 D2D/DirectWrite 上启用连字也花了我不少的时间。连字就是现在很多人喜欢的那种把多个字符(比如->、==)画在一起的那个功能。
要实现连字,首先需要通过 TextAnalyzer 分析输入的文本,得到一组 Glyph 的索引,以及一组 Cluster Map。Cluster Map 里面村的就是 Code Point 到 Glyph 直接的对应关系。比如,如果 Cluster Map 的内容是 [0, 1, 1, 2, 4],那就代表第 1、2 个 Code Point 对应了一个 Glyph(比如 Ligature 的情况),而第 3 个 Code Point 对应了 2 个 Glyph (比如阿拉伯文上面的各种修饰)。
Codepoint Index Glyph Index
0 ----------- 0
1 ----------- 1
2 ----------/
3 ----------- 2
\---------- 3
4 ----------- 4
最后使用 DrawGlyphRun,把几个 Glyph 一起画出来。如果字体提供了连字支持,那么在 Cluster Map 中就会有所体现。
C++/CLI 做胶水
有时需要跟 Native 的 Win32 API 做交互,这个时候就会感叹有一个 C++/CLI 这样的语言做胶水实在是太方便了。但 C++/CLI 的 Bug 也不少,有时会遇到报编译器内部错误,建议我把 XX 行附近的代码换个写法🤣 现在在 dotnet core 的趋势下,大概 C++/CLI 历史任务也快圆满完成了,可是替任者在哪呢?
关于这个项目的一些回忆大概就这么多了。最后还是要感慨一下,跟调原生 DirectX API 相比, 用 SharpDX 确实舒服很多,这可能跟 C# 的语法糖们也有关系8️⃣。