开发输入法是一种妥协的艺术

很久很久以前,我还没有在用 Linux 的那个时候,那会儿还在用 Windows 上的搜狗输入法。有时候经常就会好奇一件事,为什么输入法的更新日志里,经常会有「修复和XXX程序的兼容性」,「修复和YYY程序的兼容性」,难道他们不应该采用一套统一的接口吗?

几年以后当我开始在 Linux 下开发输入法之后,我终于了解了一切……

有些东西其实还是颇为为难的,我就来说一下曾经处理过的bug(有些解决,有些未解决,或者难以解决)。

1、LibreOffice 的 crash

简单来说原理就是在某个 Qt 调用 IM Module 的函数里调用某个函数可能会导致无限递归。这个相对来说是比较容易修复的,只需要把 IM Module 调用那个函数的移动到事件队列里延迟调用就可以避免递归了。但这个问题并不会在其他任何 Qt 程序中出现,相信也只是在某个 LibreOffice 的版本无意之中导致的。说实话这种问题并没有一种办法归结为是谁的“过错”,毕竟我相信相关的 API 并没有相应的文档来定义这么细节的调用是不合法的或者可能导致问题。

2、Gtk 的 Client Side Input Panel 的语言 tag 失效

Fcitx 5 有一个功能,让 Pango 优先用指定语言的字体显示文字。例如骨字在日语和中文中的字形是不一样的。Fcitx 5 中可以让日语输入法用日语字形,中文则用中文字形。Gtk 的 Client Side Input Panel 和 Fcitx 5 的 Classic UI 使用了几乎相同的代码,但是出于某种未知的原因,不知道从什么时候开始这个功能在 Gtk 的 Client Side Input Panel 就失效了。我尝试了半天最后采取了一些 Workaround 来暂时回避。

3、zwp_input_method_v1 的支持问题

Wayland 的输入法协议之前也有很多地方吐槽过了,但让我出乎意料的是 KWin 采取了非常混搭的做法使用这个协议(并非不行,只是确实很意料之外)。首先介绍一下背景,Wayland 原生输入法协议是分为两个部分的,一部分是程序和 compositor,另一部分是输入法和 compositor 之间。程序和输入法之间的协议是 text-input 系列,而输入法和 compositor 之间则是 zwp_input_method 系列。

问题是 text-input-v1,v2 是和 zwp_input_method_v1 配套设计的,而 text-input-v3 和在野的 zwp_input_method_v2 是配套设计的。当然,考虑到 wayland-protocols 里面只有 zwp_input_method_v1,以及 kwin 的这个支持是给 Maliit 用的,也是可以理解的。但 text-input-v3 的 API 变化在某些方面和 zwp_input_method_v1 就有些格格不入,总之就是凑吧凑吧凑合能用。

但我从 5.23 开始关注这个,结果到最后我给 KWin 和相关的库写了接近30个PR。每当想起这段我就在想,我好好一个输入法开发者怎么就去写 Compositor 了。

这一部分按下不表,而 text-input-v2 有一个特别的事件,为了隐藏虚拟键盘,可以请求 compositor 来隐藏。但这个功能在桌面用键盘的输入法就会导致输入窗口永久被隐藏起来。我琢磨半天想出了一个 workaround,但这个 workaround 却又反过来会导致 weston 出问题。

后来幸好我又想出了另一个 workaround 才同时支持了 weston 和 kwin。zwp_input_method_v1 已经属于很故纸堆的协议了,也不会得到今后的更新。但很多行为其实都没有明确的定义,幸而实作只有两个,因此很多实现我就变成依靠他们实现的行为来决定。

4、Chrome 的 Wayland 输入法问题

Chrome 的 Wayland 输入法一直一个浆糊的状态。首先,Ozone 早期不支持 Gtk IM Module,只支持 text-input-v1 实现了一个输入法。现在问题是,text-input-v1 支持的 Compositor 只有 Weston,也就是说支持几乎等于 0。最广泛被支持的 text-input 是 v3,KWin wlroots GNOME 都支持。

然后他们就搞了一个支持 Gtk 4 的 IM Module,但问题来了,他们的所谓支持采用了一个非正常的使用 Gtk API 的方式,导致了 Gtk 自己的 text-input-v3 实现和 Fcitx 5 的 Client Side Input Panel 都无法正常使用。后来我倒是想通了,在最新版的 Fcitx5 Gtk 里可以略微做一个检查来临时禁用不能用 Client Side Input Panel 的情形。这种情况下好歹能显示一个位置错误的输入框。

而且根据 Chrome 的架构,他们是不可能用 Client Side Input Panel 的,因为他们内部就没有真实的 Gtk 窗口来绘制(对比 Firefox,Firefox 是用 Gtk 创建的窗口)。他们唯一正常支持输入法的希望应该就是把 text-input-v1 改成 text-input-v3(或者同时支持两个)。

5、Firefox 98alpha 的输入法问题

https://bugzilla.mozilla.org/show_bug.cgi?id=1751339

这个问题倒是 Archlinux CN 的群友优先发现的,幸好发现的早,早早的就通过 bisect 定位了导致的问题。虽然,我并不能理解这个改动牵扯了什么 Gtk 内部的问题导致窗口的销毁再重建,以至于 Gtk IM Module 接收到的是一个销毁了的窗口,但反正 Revert 回去了现在没问题了。

6、神秘的 Twitter 输入框输入法问题

说起这个问题,不得不说这个问题折磨很多人很久。而且不管是在 Chrome 还是 Firefox 上都有不同的表现:https://bugzilla.mozilla.org/show_bug.cgi?id=1735227 https://bugs.chromium.org/p/chromium/issues/detail?id=952181

最后追溯追溯追溯到底,竟然追溯到了这个输入框所用的 Javascript 上 draft-js 昨天我反正顺便加了一笔我的研究,但相关的在不同平台(含非 Linux)上至少我就能找到 4 相关汇报:

https://github.com/facebook/draft-js/issues/3109

https://github.com/facebook/draft-js/issues/2227

https://github.com/facebook/draft-js/issues/1320

https://github.com/facebook/draft-js/issues/1301

我一个输入法开发者,怎么就去看 React js 了。但光看这个 bug 的时间,我怀疑已经存在了非常长的时间。不管在什么平台上,输入法用户都是挺没有话语权的。

7、SDL2 和 Fcitx 5 的输入法小问题

这个说到底还是我自己弄出来的,我几年前给 SDL 加的 Fcitx 5 支持,在传递 DBus 参数的时候,传了一个错误的 int 类型,导致访问了无关的内存,导致了发送给 Fcitx 5 的 Capability Flag 可能意外包含 Client Side Input Panel 的 Bit 导致无法正常显示输入框……而很多分发 SDL 的是不会立刻获得更新的(甚至永远不能),例如 steam 的游戏。

所以结果就是我决定在 Fcitx 5 内部加一个 Workaround 来解决这个 bug(检查某些 bit 是否有不正常的数值,如果有,则抛弃)。总之,凑合能用。

8、SDL2 的最近的其他小问题

有一个人来汇报他没法在 Dota 用输入法,结果发现是他设置的某个快捷键无效。向下研究,则是 SDL 没有发送所有按键事件给输入法。然后我测试着测试着,就发现 SDL 的 ibus 实现也有 bug(因为 Fcitx 5 支持模拟 IBus,所以我顺手这么一测……),SDL 的 Wayland 输入法也有 bug,SDL 的 Wayland 按键处理也有 bug……

然后就又给 SDL 写了 4 个 PR ……

我一个写输入法的怎么又去看 SDL 代码了。

Posted in 日志 | 5 Comments

Fcitx 5 and multiple wayland compositor

Actually there is nothing so special about this feature, it’s just come freely with the multiple X server support. I just realized that WAYLAND_SOCKET passed from compositor can be just sent via DBus to the actual fcitx5 server, without the need to let compositor start Fcitx 5 directly. There will be a new small tool included in the next release (fcitx5-wayland-launcher). It can be used to initiate a new wayland connection from Fcitx, even if WAYLAND_SOCKET is required to be used link kwin or weston.

Some people may find it useful if they want to keep Fcitx as a user-level daemon. I didn’t test it, but I think this is what can be done:

  1. Remove all other method of auto start mechanism.
  2. create a user systemd unit, with command fcitx5 -k. The “-k” allows fcitx 5 to be kept running even if display server is gone.
  3. Make your X11 or wayland desktop to run a command to start a new X connection or Wayland connection.
Posted in fcitx development | Tagged , , | Leave a comment

One year in Fcitx 5

Fcitx 5 first release is in 2020/11/2, and as of today, we have reached the 13th release of Fcitx 5.

What happened to Fcitx 5 within this whole year of development?

1. Best Wayland support on Linux

As of today, to my knowledge, Fcitx 5 is the only input method frameworks under Linux that works under all different types of wayland.

We have been testing KDE/GNOME/Sway (Only causally played with wayfire and since there are too many wlroots based around we can’t do much test there.).

2. New engines

By the end of year we have two new engines https://keyman.com/ and https://github.com/OpenBangla/OpenBangla-Keyboard .

3. Flatpak support

Not only flatpak support fcitx5, but fcitx5 also works as a flatpak package.

https://fcitx-im.org/wiki/Install_Fcitx_5#Install_Fcitx_5_from_Flatpak

4. New features comparing to Fcitx 4 counterparts.

We really got tons of them new features, e.g.

  • fully customizable shuangpin profile (you can define shuangpin profile with any combinitions of initial/final
  • new preedit mode that takes less space in input window
  • punctuation configuration via GUI
  • quick match for long word in Pinyin
  • paritial shuangpin support
  • rime plugin loading
  • rime new preedit mode
  • rime dbus API
  • rime app_options

5. Android support

I didn’t develop this, only provides some Q/A on the details, and add some support to make it build/work on android more easily. https://github.com/rocka/fcitx5-android-poc/ It’s still considered as highly experimental, please use it with caution. One of our next target is to come up with a new API for implementing virtual keyboard, which will also benefit the native linux on screen keyboard.

If you are still using Fcitx 4, please consider give Fcitx 5 a try. You can find the distribution that packages fcitx5 via https://pkgs.org/search/?q=fcitx5 .

Posted in fcitx development | Tagged , , , | 1 Comment

Why surrounding text is the worst feature in the Linux input method world

This is mainly a complain about how mess this feature is and why no one could reliably use it.

To give people some background, surrounding text is about the feature that an application can notify the input method what are the characters around the cursor, and the input method can directly change the text around the cursor.

For example, in a input box, you have some text like this.

With surrounding text, application is able to notify input method the context around the cursor.

For example, in this case, the input method will receive text is “I like typing.”, the anchor is 8, and the cursor is 10. Anchor is the starting offset of the selection, and the cursor is the end of selection. If there is no selection, anchor will equal to cursor.

Now you may want to ask, isn’t it a costly thing to do? Answer is YES. Imagine you have a crazy long line in the editor, and whenever you change the text, you will need to send it over to the input method. Usually, input method would just apply a maximum size.

Next we will need to talk about the messiest thing about this is the API. Here lets list things about what are people doing with it.

  1. Gtk native API: set_surrounding_text / delete_surrounding_text, the value of offset is Unicode(UCS4) character based. delete_surrounding_text uses (offset, length) to define the range.
  2. Qt native API: the value of offset is UTF-16 character based. delete_surrounding_text uses (offset, length) to define the range, but, it excludes the current selected text when applying offset and length.
  3. Wayland protocol text-input-v1 / zwp_input_method_v1, similar to Qt, but offsets are UTF-8 character based.
  4. Wayland protocol text-input-v2 / text-input-v3 / zwp_input_method_v2 , delete_surrounding_text uses (before, after) to described the range. Basically it means some additional character before and after the selection. Offsets are also utf8 character based.
  5. Gtk implementation of text-input-v3 (?!), does not follow (4), by just using received UTF-8 offset as Unicode (UCS4) based offset, which is actually a bug. Also, it does not actively sending over the update of surrounding text, which makes it useless.

Also, people seems to not have a clear definition about whether surrounding text should include preedit text. Which is purely headache to deal with.

Not to mention that non-native widget implemented with Gtk/Qt are very likely to implement it in a wrong way. Also XIM does not support it. Not to mention that terminal application that does not support it have to claim it support surrounding text, due to lacking of ability to notify application.

So now, people are more likely to stick to use a limited set of feature in surrounding text.

  1. Use it as auxiliary data like primary selection, to just learn about what text is being selected.
  2. Delete surrounding text only when it is extremely reliable, e.g. delete 1 cursor before cursor.
  3. When implementing a feature that requires full featured surrounding text, make this feature optional and always provides an alternative easy way for user to not using it.

Posted in fcitx development | Tagged , , , , | Leave a comment

上下跳动的文字

我觉得我自从手搓几个low-level的文本绘制之后这个问题其实已经变成专家了。问题本身其实还是很有趣的,这里特别总结一下。

不少人可能都注意到了 Linux 下面有时候会有一种奇怪的现象,就是你输入文字的时候,一旦输入了汉字,汉字可能会把整行文字的位置降低。删除掉汉字的时候,则又会恢复到原本。这是为什么呢?

其实道理是很简单的,就是你这一行文字混合了两种不同的字体。为什么明明没有单独配置但是却使用了不同的字体?因为系统默认使用的英文字体本身,可能并不包含汉字。因此当需要显示汉字(或者其他任何不存在于这个字体的字符)时,就只能回落(fallback)到另外的字体上。你也许注意到了,即使两种字体配置为同一个大小(point),但是实际显示在屏幕上的大小可能是不同的。当两个字体同时在一行文本中使用的时候,为了将他们对齐,则需要使用一个标准将两个字体的文本对齐。这个标准就是基线(baseline)

A diagram showing the line terms used in typography, with the baseline highlighted.
图片来自 Wikipedia

这里还有两个重要的线 ascent 和 descent。ascent 就是从 baseline 到字体的最高点,descent 就是 baseline 到字体的最低点。可以假想一下,当另一个字体混入的时候,如果它的 ascent 高于原本字体的 ascent,自然就需要将文本整体“下推”。

gedit

通过辅助线,我们可以明显观察到 DejaVu 和中文字体组合之后可以出现明显的下沉现象。

对于编辑器来说,只有两种方式,一种允许每行的高度不同,从而适配文字本身的大小, 另一种则是每行高度相同,但带来的副作用可能是会对 fallback 到的较高的字体砍头去尾。

这里有一个展示了 katepart 曾经长时间存在的文字渲染问题,当字体可能下推文本过多时,超过了原本的行高,则不得不砍掉了一些。右侧是经过我修复之后的 katepart。

这里所谓的修复是什么呢?其实非常的简单,就是把“下推”的文字拉回原本只有单个字体的基线位置。简单的来说,就是进行这样的运算:在 y 轴上偏移一个这样的数值:(渲染文本的整体 ascent – 字体本身的 ascent)。

这样运算之后,那些并非 fallback 的字符将不会被「下推」,而保持在原始的位置。fallback 到其他字体的字符则会对应的上拉一些位置。你可能会问了,这样似乎并没有解决字体被砍头的问题啊?确实,但字体设计的时候,一般会在顶部留有余地不会紧密排列,经过这样调整之后,反而会让大部分即使是 fallback 的字符也完整显示在行内,同时还会避免跳动的问题。

同时,如果你多次使用文本绘制 API 来绘制多条文本,这样也可以保证他们自动会对齐在一起。否则则可能出现即使逻辑上是一行,但是因为文本不同而上下参差不齐的问题。

Posted in Linux | Tagged , | Leave a comment