Linux 的 HiDPI 配置问题

其实这个问题对我来说从来也不是个问题,因为我已经基本深入理解了这个问题的本质……然而似乎有不少人对于怎么配置,为什么这么配置,为什么又出了问题一知半解,因为最近在 Twitter 上回复一些比较复杂的内容,特此整理记录一下。

在过去,电脑的屏幕一般都是 96 DPI,DPI 的含义是每英寸点数(像素点),具体计算的方法也非常简单,用像素的个数除以屏幕的长宽即可。随着技术发展(显示器和处理器),电脑逐渐可以支持更高的 DPI 用于显示。抛开具体发展不提,但是这里引入了一个问题,你程序并不支持在这样的屏幕上正常显示。通常来说的问题就是界面特别小。

为什么?

因为程序过去从来也不需要考虑这个问题,都是按照像素来进行的设计,例如显示一个 100 x 100 的窗口,在 96 DPI 上大小正常,但是 DPI 翻倍的话,就只有原来的 1/4 大。人的眼睛在乎的是它现实中有多大(像素 / DPI = 长度),而不是有多少像素点。为了让程序能够正常显示,同时又不需要修改太多代码,大家也是努力想了很多办法。

1、设置字体大小。

界面框架大小不提,字看不清那是最难受的,因此只调整字体大小这样的设置,成为了大部分人解决问题的第一步。但是,在 X 上,这个设置是全局的,因此也就不能针对不同的屏幕,来设置一个统一的 DPI。一个屏幕大一个屏幕小也可以说是常态了,不少人就选择设置一个居中的值来自己适应。毕竟人是活的,程序是死的,改变不了世界那就只好自己适应世界。

2、程序来支持内部缩放。

这里,当然也就是 Qt / Gtk 这样的大框架才有精力和能力去支持这种功能。这个功能在程序不需要进行额外修改,或者只进行少量修改的前提下,直接在内部添加坐标的转换功能,乘以一个倍数,变相将程序放大。这样,在高分辨率的前提下,就可以用正常的大小显示界面。

3、设置分辨率来进行缩放

也许有人说,显示器不都可以配置分辨率么?你3200×1800的屏幕,我强行设置 1600×900 不就完全正常大小了么?这样说当然是没有问题,但是相对的你也就没有利用上这个屏幕的资源,原本你可以在更高的像素点上进行更好的反锯齿。相当于说,你把大图片缩小再放大,你得到的能是原始图片的效果么?必然是有数据损失的。当然,还是那句话,人是活的,程序是死的。一句 I don’t care 什么问题都能解决。

上面说的分辨率缩放只是在配置当中其中的一个方向。相反,还有故意把 1600×900 伪装成 3200×1800 这种操作,这是为什么呢?

这是因为,尽管在 2 中你可以靠 Toolkit 来放大程序的界面,但是如果你同时有两个不一样的屏幕,那这可就倒了大霉了。一个屏幕 96 DPI 一个屏幕 289 DPI,那你的程序是选哪一个 DPI 来显示呢?

关于这点,其实甚至还没有到「选哪个」的问题,支持根据屏幕动态切换 DPI 的 Toolkit 在 Linux 上只有 Qt 这一家。Gtk 根本就不支持动态在运行时根据屏幕切换 DPI。那你既想要显示“锐利”,又想要大小正常,那就只有一个办法了。把这个 96 DPI 的屏幕也变相变成 HiDPI 的。尽管你没有 HiDPI 的屏幕,但是你可以通过软件「装作」你有这么一个屏幕。

xrandr --output eDP-1 --scale 2x2 --mode 1920x1080 --pos 0x0 --output DP-1-0 --mode 2560x1440 --pos 3840x0

上面这条命令,就是把 eDP-1 在软件层面变成 3840×2160 的屏幕,放在左边,尽管它原生只有 1920×1080。这样有两个 DPI 相近的屏幕,你自然就可以对按照 2 对程序设置统一的缩放倍率了。

4、将他们组合在一起

先来说说 X 到底是怎么回事。对 X 来说,它不知道窗口里面什么 HiDPI 不 HiDPI,你给我什么,我就显示什么。所有坐标在 X 层面都是原生的。也就是说,它没法把不支持 HiDPI 的程序放大,例如一些 Java 程序或者 Xlib 程序。那怎么办?如果你非要一切程序都正常大小,那就只能走暴殄天物的方案 3 ,把分辨率人工降下来。

还是再重复一次,「人是活的,代码是死的」。你当然可以选择「那些不支持 HiDPI 的程序都是辣鸡,我不用了!」。为了享受「锐利」,你选择了只用 Gtk / Qt 的程序,其他程序如果能用调整程序内部字体凑合就凑合一下,凑合不了干脆不用,反正 Gtk / Qt 的选择那么多。

那么在你选择这条路的时候,首先就来了解一下 Gtk 和 Qt 设定上的不同。Gtk 的 scale factor 是影响字体大小的,而 Qt 的是不影响字体大小的。

通过下图,你可以很容易对比理解 Qt 和字体 DPI 组合的行为。

https://pbs.twimg.com/media/EvcWJYPVIAEo0x_?format=jpg&name=orig

然后再来对比一下 Gtk 的行为。

图像

也就是说,如果你想 Qt 和 Gtk 同时达到两倍缩放,并且大小相当,需要设置:

1、Xft DPI  192
2、QT_SCREEN_SCALE_FACTORS=2
3、GDK_SCALE=2 GDK_DPI_SCALE=0.5

具体数值你可以根据你的屏幕自行修改。当然,这些数值具体的设置方式可以通过环境变量,又或者可以通过桌面环境的设置,但不管通过哪种手段,本质其实是一样的。

在这个设置的基础上,又可以分别分成两个路线,使用 3 的方法伪装低 DPI 的屏幕,又或者你连 Gtk 也一起放弃,采用 Qt 可以支持不同屏动态切换 DPI 的设置。

QT_SCREEN_SCALE_FACTORS="eDP-1=1;DP-1-0=2"

例如这样设置的话,就可以在 eDP-1 用 1 倍缩放,DP-1-0 用两倍缩放。尽管 Xft DPI 还是只能一个值,但是 Qt 会自动帮你绑定到某个屏幕,另一个屏幕还是采取包括字体大小在内的完全缩放,把窗口在不同屏幕移动的时候也会自动调整程序的大小。当然为什么只有 Qt 实现了这个我猜想主要是因为 Windows 上只有这一种方案可以总是保持原生状态渲染,就顺带着也支持了 Linux 。

如果说到 Wayland(目前已有的实现),本质上是一样类似的,但是参数实际上就没有这么多花活可以弄。Wayland 对于窗口可以有一个窗口本身的 scale 是多少的属性,当然,得是原生 Wayland 窗口,X wayland 的窗口 scale 会自动当成是 1。然后这个窗口会在不同的屏幕上,根据屏幕的 scale 进行匹配缩放。这也就是为什么 Wayland 可以支持所谓的「分数」缩放(非整数百分比,例如 150%)。其实 X 如果你把 xrandr 的 scale 改成带小数的倍数也是可以的。

其实 Qt 在 X 下,也是可以支持非整数的 scaleFactor。但是这里有一个问题。浮点数的计算是有精度损失的,所以如果你原生进行内部坐标非整数的运算,实际上更有可能出问题,在哪里界面上缺掉一像素也是常见的事,相比整数倍数运算之后再缩放来说的方案,问题会少一些。但是希望大家还没有忘记 Wayland 下也还有X wayland 需要显示。根据之前说的情况 X Wayland 和 X 并没有什么差别,只是 3 中描述的操作现在交给 wayland compositor 进行了。GNOME 默认的行为是不缩放 X,可以通过 gsettings 启用一个实验性功能来让 X 被「放大」。KDE 是默认采用设置的倍数放大。相对来说,反而和 X11 相比更加不「自由」,并不能分开指定 Wayland 和 X 的缩放倍率。

毕竟,对有些人来说,显示锐利本身比大小更重要。左侧是原生 Wayland,右侧是 X wayland。尽管大小一致,在 150% 缩放的情况下 X 的程序自然是模糊了(从 100% 放大到 150 %)。而 Wayland 这个则是从 200% 缩小到 150%(当然,也许用 300 % 绘制再缩小 150 % 效果可能更好?但具体来说现在系统实现就是这样的)

https://pbs.twimg.com/media/EvcdZv1VIAQ4lkM?format=jpg&name=orig

整个 HiDPI 问题就是那么一个问题,怎么把原本针对小屏幕设计的窗口放到高分辨率上显示,和 X 还是 Wayland 有什么相关么?没有,只是在不同平台你有相对不同的变量可以操纵。有没有完美的解决方案?当然也是没有。你偏要把一个 100×100 的窗口当成 200×200 显示,窗口不支持缩放当然只能模糊。窗口支持缩放却又不支持动态切换,混合分辨率的在低分辨率上那就只能浪费资源来缩放,这本来就是两难的问题。真正完美的纯动态切换现在也就只有 X11 + Qt,但大部分人也不是靠纯 Qt 程序不是?非要概括的话,就是有钱能解决大部分问题,有钱就买俩 HiDPI 屏幕,有钱就硬件强力,浪费资源缩放也无所谓。

Posted in Linux | Tagged , , | Leave a comment

Fcitx 5 开发(三)如何调试输入法,先有鸡还是先有蛋

调试输入法,和写别的程序其实没有本质的区别,常见的办法也就是 GDB 和输出日志。但它特别的地方主要在于,你用来调试输入的程序也是输入法的客户端之一,在调试输入法的时候,如果不进行一些特别的操作,将会导致你调试用的终端 Freeze(把按键发送给了输入法,却因为程序 GDB 暂停无法获得响应导致 Freeze),也就是标题所说的先有鸡还是先有蛋的问题。

当然,要解决这个问题其实也简单,就是让你的终端不用输入法就行了。这里有几个点需要注意,常见桌面环境的终端一般是单实例,你即使启动一个新的窗口也是同一个进程,并不是重新启动一个进程。所以一般我个人会选择使用另外的终端程序。这是从终端程序禁止输入法方面来说。反过来,也可以通过命令行参数禁用 Fcitx 的前端。来阻止 Fcitx 和对应类型的程序进行通信。

先说说怎么启动一个无法使用输入法的终端。如果是 XIM 的终端,例如 xterm,alacritty,就直接用 XMODIFIERS=@im=none xterm 这样的方式启动,启动之后,在终端里面直接启动新的 fcitx 的话,就需要注意要在终端里面重新恢复 XMODIFIERS 的值为一般的值。因为根据 XMODIFIERS 的处理机制,Fcitx 会根据它启动时的 XMODIFIERS 设置对应的值。如果不进行设置的话,当前的这个终端自然也反而会可以和 Fcitx 通信了。如果是 Gtk 程序,可以设置 GTK_IM_MODULE=gtk-im-context-simple ,如果是 Qt 5程序,可以设置为 QT_IM_MODULE=compose 。

如果想要从 Fcitx 这边禁用输入法,那就需要利用 –disable 命令行参数。我常用的一般就是 fcitx5 --disable dbusfrontend,ibusfrontend。你也可以根据你具体的需要来设置禁用对应的 Frontend。

当然,这个并不是总是特别好用的。因为这里牵扯到第二个问题,就是程序的焦点问题。在你切换到终端的时候,你用来测试输入的程序就失去了焦点,会导致程序直接进入重置状态,很有可能无法达到原本想要调试的状态。这里就可以采取第二种方法,启动一个新的 X server,从而避免焦点抢占的问题。常用的一般就是 Xephyr。

可以用如下的命令来设置启动这样的一个环境

Xephyr :1 &
DISPLAY=:1 openbox & #启动一个窗口管理器
DISPLAY=:1 xterm & #启动一个调试用程序用于测试
DISPLAY=:1 gdb --args fcitx --disable dbusfrontend,ibusfrontend # 启用一个禁用 dbus 的 fcitx

这里需要注意的是,dbusfrontend 并不会因为显示服务器的隔离而无法进行输入,所以例如你要在 konsole 上进行调试,如果不禁用 dbusfrontend 的话,console 还是会收到按键。

所以如果你想要调试一个使用 dbusfrontend 的程序,就最好还用上前文所述的办法,在主显示服务器启动一个无法使用输入法的终端来进行调试。

Xephyr 不会因为它自身焦点改变而修改 Xephyr 里面运行的程序的焦点,就可以很方便的在 Xephyr 和外部终端之间来回切换调试一连串的输入行为了。

之后,你只需要在 Xephyr 里面进行键盘操作,在触发对应 gdb 断点之后,用鼠标切换到外部的终端继续进行调试即可。调试完成之后也可以用鼠标切换回到 Xephyr 里面再继续模拟另一些用户的操作。

调试来说,就还有传统的输出 Log 的方式。简单的来说用 FCITX_INFO() << item << to << output; 就可以输出日志了。和 std::cout 类似,但是优点就是可以直接支持很多模板类型,例如 STL 的容器和 Fcitx 自身数据类型的直接输出,更加的方便。

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

Fcitx 5 开发(二)一个按键事件的后半生

当按键被 Fcitx 的前端接收到之后,首先它需要找到对应的输入上下文,然后创建对应的按键事件结构:

https://github.com/fcitx/fcitx5/blob/c492be884540b55ee590d0e3312c4889c8bf4300/src/lib/fcitx/event.h#L242

首先对于一个按键来说,我们先要区分和按键相关的好几个不同的值的含义:Code,State,Symbol。Code 表达的是这个按键对应的物理按键,例如键盘上的 A,不论你在什么情况下按 A(Caps Lock / Shift),产生的 Code 是一样的。State,表示了按键发生时键盘的状态,包括了锁定键的状态,比如 Num Lock 和 Caps Lock,还有各种修饰键(Modifier)的状态是否被按下。当然,对于 32 个 Bit 来说,键盘并没有那么多的按键,因此有一些位在 Fcitx 中也被挪作他用,例如 https://github.com/fcitx/fcitx5/blob/c492be884540b55ee590d0e3312c4889c8bf4300/src/lib/fcitx-utils/keysym.h#L51 这个就是新加入的按键是否是重复键的。

Symbol 则是表达了经过键盘布局转换之后得到的符号,一般来说,输入法的逻辑是直接和 Symbol 打交道的,而不关心从 Code 到 Symbol 之间的转换。Fcitx 的 Symbol 对应的数值是采用和 X11 Symbol 一样的值来简化判断,这样在大部分情况下都不需要重新进行映射。

当创建好对应的 KeyEvent 结构之后,就会把他放到整个 Fcitx 的事件处理当中。这里的事件仅表示和 Fcitx 主体逻辑相关的事件,都是抽象的和 Fcitx 自身逻辑相关的(例如界面是否需要更新 / 发生了按键 / 提交文本),不可和 Fcitx 的事件循环(仅和 IO 相关)混为一谈。

具体来说,则总是通过 Instance 类的这个函数来处理一个新的事件 https://github.com/fcitx/fcitx5/blob/c492be884540b55ee590d0e3312c4889c8bf4300/src/lib/fcitx/instance.h#L66

里面具体处理分为三个阶段,因为输入法是核心,所以事件处理的顺序是围绕着 Engine 构建的。总共分为“在输入法之前,输入法,在输入法之后”三个阶段。当然,内部还有保留的 ReservedFirst 和 ReservedLast 用于保证某些特别的事件会总在最初或者最后被执行,一般用户不能使用这两个阶段。

需要关注某些事件的模块,可以通过 https://github.com/fcitx/fcitx5/blob/c492be884540b55ee590d0e3312c4889c8bf4300/src/lib/fcitx/instance.h#L70 来注册回调函数,这样就会在对应的阶段被调用。

所有的事件都有两个属性:accepted 和 filtered。accepted 控制的是之前 postEvent 的返回值,而 filtered 则控制了事件的处理流程,当一个事件被 filter 之后,它之后的回调函数则都不会被调用。通常这两个是同时设置的,因此有一个 filterAndAccept 来同时进行这两个操作。

这里事件里面,按键事件是最特别的一种,只有它可以被 filter,其他大部分事件则都不可能被 filter。这主要是为了保证逻辑上这些事件处理的一致性,而避免导致问题。

其中,Fcitx 自身的逻辑,也都是构建在这些 API 之上的。例如切换输入法的按键等。当按键一个个经过不同的 handler 处理之后,得到的结果将根据不同的 frontend 返回给不同的程序。对于按键这个事件本身而言,也只有两种结果,处理或者不处理。从接口的角度来说,实际上并不那么需要设计成处理 / 不处理,而只需要在不处理的时候生成一个按键事件给程序就可以了。只是在实践中,不同的 Toolkit 对于生成事件的态度差别太大,因此维持原始事件的数据结构的状态是一个保证许多功能正常工作的方式。因此返回的结果也才变成了“处理/不处理”这样的情况。

在 Fcitx 5 中,系统的键盘布局和分组是绑定的,如果在分组中使用其他的布局的话,会中间再经过一个只对 Fcitx 自身可见的转换。这也就是为什么 KeyEvent 中有三个不同的按键结果:

1、OrigKey:来自原始的系统键盘转换的结果
2、RawKey:经过 Fcitx 内部布局转换的结果
3、Key:Key 经过 Normalize 之后的结果。当然,这个 Normalize 并没有什么神秘的,而是方便输入法实现自己的功能。例如,把按键的 Modifier 过滤为只有几个常见组合键的状态(去掉 CapsLock / NumLock 等)。对于输出字符的按键,则去掉 Shift(避免 CapsLock 下的 A 和 Shift + a 判断需要不同的代码),简单来说就是方便输入法实现的小功能。

输入法或者其他模块可以根据自身逻辑处理按键,并根据需要更新界面信息。和 Fcitx 4 不同的是,每个输入上下文的状态是独立的,其中也包括了界面相关的信息。这在 Fcitx 4 中则是全局共享的。这也主要是为了支持在多个显示服务器下可以采用多个焦点,不互相冲突。

界面根据对应输入上下文的显示服务器,则会选择对应的界面进行显示。例如 Fcitx 5 可以支持同时连接不同的 X 服务器,界面就会根据具体的 X 服务器在对应的服务器上进行渲染。(主要指 classicui 的实现支持这个功能)。

Posted in fcitx development | Tagged , | Leave a comment

Fcitx 5 开发(一)一个按键事件的前半生:由程序到输入法

因为觉得有必要趁着 Fcitx 5 发布的功夫,决定写一系列的关于如何开发 Fcitx 5 相关内容的文档,之后会更新到 Wiki 上。

要理解输入法是如何工作的,首先要了解的是程序究竟是如何与输入法进行交互的。而这一部分的内容,也与一个常见问题相关:为什么我在 A 程序里面无法使用输入法?

Fcitx 的基本架构是客户端/服务器架构,客户端,在 Fcitx 里面对应的词是“输入上下文(Input Context)”,具体究竟对应程序中的哪一个实体,是并没有明确规定的。一般来说,可能是对应程序的一个窗口。而具体对应什么,是取决于程序自己的实现,也就是“程序认为”哪些部分应当作为同一个上下文来处理。虽然,在实践当中一般并不会有程序做到如此的细化。

当一个程序不能用输入法的时候,一般最常见的情况就是这个程序并没有采用正确的方式和输入法进行通信。

那么,目前在 Linux 下到底有多少种不同的客户端通信方式呢?看看源码中 frontend 目录下有多少就知道了。

这里我们也详细说明一下各个目录的情况:

1、xim,最古老的基于 X11 的输入法协议。在 X11 下理论上是通用的,实现由 Xlib 完成。和 Xlib 的事件处理结合非常紧密,采用了一种直接原生和 Xlib 按键转化字符串的方式来实现,而不是非常直接的有回调来传递输入法想要程序进行的操作。Xlib 的实现里面非常多的操作都是实现成同步而且并没有错误处理,这也导致了一旦输入法退出,程序如果此时在进行一个同步操作,就会直接冻结。而这部分代码过于古老基本上处于没人会去改的状态,其中非常古老的缺陷之一就是光标跟随和嵌入预编辑文本无法共存,尽管从协议的角度来说,并没有什么道理阻止这个功能,但就是几年尽管有 Patch 都没有合并的状态。

XIM 允许多个输入法注册在 Root Window 上,具体使用哪个可以通过 XMODIFIERS 这个环境变量设置。Fcitx 的做法是,自己也读取这个环境变量,如果读取到了,就根据这个环境变量的值来选择要注册的名字,因此通常设置的 XMODIFIERS=@im=fcitx 这里的“fcitx”是什么并不重要,因为 Fcitx 自己反正会尝试设置和系统值一致的。

而目前很多界面都采取了一种直接去除 XIM 的态度。Qt 5 和 SDL 2 是最早去除的,而主要的原因是他们都切换到了 xcb 来处理 X11 的通信。尽管 Xlib 现在内部已经变成了基于 xcb 的实现,但是,如果原生就是 xcb 的情况,是无法反过来再用 xlib 的。而在当时,并没有一个基于纯 xcb 的 XIM 实现。我在开发 Fcitx 5 的过程中,开发了一个纯 xcb 的 XIM 实现。当然虽然现在 XIM + xcb 已经又成为可能,但是大概也没人会把他再添加回来吧。可能有一些纯 xcb 的程序也许会考虑用它来支持 XIM 输入。

2、dbusfrontend,这个就是 Fcitx 自己的协议了,基于 dbus 实现的。需要程序配合使用对应的 DBus 接口,主要就是通过 IM Module。而这个主要就是通过常见的两板斧:GTK_IM_MODULE 和 QT_IM_MODULE 环境变量控制。

事实上,即使你不设置环境变量,Gtk/Qt 也会自动挑选,但是就不一定挑选到哪个了,可能根据系统语言和文件系统遍历文件顺序选,但往往不对,因此都是要求用户设置这两个环境变量来保证输入法的使用。另外,环境变量也不止这两个:

QT4_IM_MODULE:顾名思义,就是只给 Qt 4 的程序设置的环境变量。
SDL_IM_MODULE:给 SDL 2 程序设置的
GLFW_IM_MODULE:kitty 这个终端模拟器自己发明的…目前只有一个 ibus 可以作为值设置。

在从 Fcitx 4 迁移到 Fcitx 5 的过程中,考虑到用户也许不一定要使用某个 im module,因此在同时需要支持 Flatpak 的背景下,新加了一个和 Fcitx 4 原有协议类似但不完全一样的接口(消除一些过时的接口),同时使用通用的为 flatpak 准备的 DBus 服务名。这样来让 Fcitx 4 / 5 的可以任意搭配 Fcitx 4/5 的 IM Module 进行输入。

3、fcitx4frontend 和 ibusfrontend,顾名思义,就是模拟原本 Fcitx 4 自己的 IM Module 协议和模拟 IBus 的。这里主要是出于兼容性的考虑,因此添加了 IBus 的模拟支持,意外发现这个决定还是相当正确的,因为 GNOME-Shell 自己它砍掉了 Gtk 的 IM Module 部分而直接采用了 IBus ,因此在 GNOME-Shell 的搜索框就必须用 Fcitx 5 才能正常输入。另一方面,因为 Qt 5 移除了 XIM 而内置了 IBus 的支持,兼容 IBus 也能提供对那些只 Bundle 了 IBus 的闭源 Qt 程序的支持。

而 Fcitx 4 的 Frontend 主要是出于对那些闭源又只包括了 Fcitx 4 IM Module 的程序来说的。让他们一个个都更新到 Fcitx 4 较新的 IM Module 实在不太现实,特别是上游经常反馈了也毫无动作。

4、waylandim,实现了 zwp_input_method_v1 和 zwp_input_method_v2 的支持。但不幸的是,没有什么正经 Compositor 支持。而且这两个协议对于输入法上下文都缺少跟踪能力。V1 在程序失去焦点之后就销毁上下文,V2 则根本就没有上下文的概念。因此我时常说,这两个协议反正支持了也没什么大用。而在 Wayland 里程序是使用 text-input 协议和 Compositor 通信,Compositor 再中转成 zwp_input_method ,问题就是 text-input 程序本身支持就缺乏,结果反过来就更没有程序能正常使用这个了。

有了客户端之后,客户端和 Fcitx 之间最重要的,就是按键了。在使用不同的 Frontend 的前提下,按键尽管到达输入法的途径并不相同,但是总归是输入法接受按键,处理按键,发送需要程序进行的操作这样的流程。

接下来重新针对每个 Frontend 讲述一下按键事件到达输入法的区别。

1、XIM:具体的实现细节通过 XFilterEvent 函数隐藏起来了。这里的行为是先由显示服务器(Xorg)将事件传递给程序,在 XFilterEvent 中根据目前的情况(焦点等),将按键包装在 ClientMessage 当中(另一种 X 事件类型,内容载荷可以当作纯 binary),发送给输入法。

2、对于 dbus/fcitx4/ibusfrontend ,具体的传输都是由 DBus 进行的,和 XIM 类似,程序接收到来自显示服务器(Xorg / Wayland)的按键事件之后,通过调用 IM Module 这一层抽象,IM Module 再调用对应的 DBus API 来将按键事件传输给 Fcitx。如果没有选择正确的 IM Module,则自然不会将按键发送给输入法。这也正是为什么输入法有问题时,多数情况是检查环境变量设置是否正确。

3、Wayland 则更特别一些,zwp_input_method_v1 和 XIM 类似,程序通过 text input 协议发送按键,但不是直接传送给输入法,在 Wayland 下也无法做到,中间通过 Wayland Compositor 进行中转,再由 Wayland Compositor 利用 zwp_input_method_v1 传输给输入法。zwp_input_method_v2 则不同,由输入法在需要的情况请求一个 Keyboard grab,请求之后,Compositor 则不会将按键发送给程序,而是直接发送给输入法。

但无论是哪种方式,接收到按键才能让输入法正常工作,不同的方式有一些不同的与按键事件传递不同的能力,例如是否能获取程序是什么,但其他方面则没有什么太大不同。

按键从你键盘按下之后终于历经千辛万苦终于到达了 Fcitx,Fcitx 终于可以开始处理按键了。下一篇将会主要介绍按键在 Fcitx 内部的处理过程。

Posted in fcitx development | 1 Comment

Client Side Input panel for Fcitx 5

(English version is at the end of this post.)

输入框的定位相关的问题其实很复杂,过去 Fcitx 总是自己绘制相关的窗口。X11 这个办法还好使,但是其他情况可能就没这么好使了。让客户端绘制窗口的想法其实比 Fcitx 5 要早得多,最早是为了 fbterm 而增加的支持。

显然 fbterm 没法利用任何来自显示管理器的界面,所以它需要自己绘制它自己的界面。那会儿我添加了一个很简单的实现来支持这个功能。

为了正确显示界面需要支持以下功能:

  • 窗口显示在光标的位置
  • 窗口不能移出屏幕

在 wayland 里,和 X 最大的区别就是不知道窗口的位置,所以不得不使用特别的方法来支持。

  • 用 wayland 的输入法协议的话,它确实是支持将某个窗口标记为输入法窗口并让混成器帮你进行定位,但是需要程序自己先使用 wayland 的输入法协议,混成器也要支持这个协议。目前这两部分的支持并不好。
  • 或者让窗口传送一个相对坐标,然后让混成器帮忙移动窗口。gnome wayland + kimpanel 的扩展就是这样进行定位的,因为 kimpanel 扩展正好是运行在混成器里面所以知道所有的窗口信息。但是让各种混成器都支持这个也不太现实。
  • 然后就是本次说到的方式,让程序自己绘制界面。当然,缺点就是实现通过 im module 的话,就只能支持 Gtk / Qt 的程序,但相对来说可以覆盖大部分的程序了。我个人对于这个方法比较反感,因为感觉可能出问题的地方很多,对于程序来说添加一个额外窗口和过去的 im module 相比有了更强的侵入性。不过在其他很多系统上,例如 windows 和 uim 可能都使用了这个方式。

不过相比只是因为担心就不采取行动的话,还不如先试一试,毕竟解决实际存在的问题的意义更加重大。想要测试的话需要使用 fcitx5 / fcitx5-qt / fcitx5-gtk 的 git代码。

目前还有一些实现的限制:

Gtk 3 不支持已经实现的窗口重新移动位置,Gtk 4 对于不支持较新的 xdg popup positioner 的混成器的是采用隐藏再显示窗口的方式,而这个方式会导致窗口闪烁(Qt 用同样的 hack 就不会,我猜测是 Gtk 有和刷新率对齐之类的操作?)。所以我干脆就没在 Gtk 3 用这个 hack,毕竟闪烁起来看着更糟一点…

Qt 的话出于没有暴露 xdg positioner api 的限制,在窗口定位方面并不能保证不移出屏幕,因此我采取了另一个方式:保证输入框总是在窗口范围内。这个对于小窗口并不是特别友好,例如 krunner,不过我也没有更好的办法了。

本文就是用 firefox + wayland 输入的,整体来说还行……Gtk 3 定位延迟虽然有点难受不过反正我也没更好办法啦。

In the past years, fcitx always renders its pop up on by itself. This is Ok for X11, but not so good for other cases. The idea of letting client to render its UI actually existed long before fcitx 5 comes out. It was first introduced in fcitx 4 for fbterm.

Obviously, fbterm can not render UI using any display server, so it need to render the UI by itself. At that time I added an adhoc implementation for it to display a UI with a minimum implmentation.

In order to properly display an input window, there are certain features need to be supported:

  • Input window should be positioned right beside the cursor poistion.
  • Input window should not be placed out of screen.

In the wayland world, things is also very different from X. Without the knowledge of window positioning, we need to have some different way to support this.

There are current multiple approaches being used with in Fcitx:

  • With zwp_input_method, the wayland protocol, it has support to declare a surface to be input panel window, and compositor will position it properly. Unfortunately, this requires client to use wayland and text-input protocol, which is not yet well supported by the clients.
  • Or, with im module, send over a relative coordinates and ask composition to position the window with this “relative” coordinates instead of traditional absolute ones. This is used by gnome wayland + kimpanel extension. Luckily, gnome-shell extension runs within compositor, which means it has all information about the windows, e.g. which window currently has focus, window position. It can also to freely move the window created by extension. But this won’t scale anyway, because it’s unlikely to make all the compositors to support the same thing.
  • Then we can still ask client to render the window, just like fcitx-fbterm. This is an intrusive way for the application, so I never tried this approach before. Just FYI, this approach is being used by many other systems, for example, Windows, So it is not an rare solution. I also heard that uim is using such approach. But it also has its downside: duplication of code for rendering, inconsistency between toolkits, only support Gtk/Qt, more likely to have problem for certain weird application.

There is no silver bullet to this problem, but I always want to help our users to type text more freely, regardless of which desktop they want to use.

So free free to give it a try, you’ll need git from fcitx5 / fcitx5-gtk / fcitx5-qt. So far I tested many applications, including libreoffce, firefox that I suspect may have problem, but so far it looks good.

There still some limitation in this implementation:

  1. Gtk 3 doesn’t support xdg popup reposition, while this is not yet supported by a lot of compositor, Gtk 4 workaround this automatically by show and hide the window. But if I use such workaround manually in Gtk 3, it will cause the window flicker (Similar code in Qt won’t, which is really weird. My guess is this is related to frame alignment or sth.).
  2. Qt doesn’t expose all the possible interface for xdg popup, which means the position can’t use the slide / flip parameter like Gtk to ensure it is within the screen. So I took another approach here: ensure the popup window always within the range of original window. This doesn’t work well for tiny window like krunner, but I have no other good solution for it. Qt 6’s wayland is able to get the native interface for xdg popup, so it might be possible to do some manual coding there.
Posted in fcitx development | Tagged , , , | 3 Comments