読むWM "TinyWM" をちゃんと読む - X11
この記事について
この記事は、TOGATTA SERVER LT Vol.4にて発表したスライドをZenn向けに公開したものです。
Marpで書かれたスライドの原稿がベースなので、書いている文章がスライドっぽかったりします。ご了承ください。
概要
「WM(ウインドウマネージャ)」 のミニマルな実装であるTinyWMを読んで、X11におけるWMの振る舞いを理解しよう
前提知識
X11 (X Window System) とは
UnixおよびLinux系OSでよく使われるウィンドウシステムの一つ
- GUI環境の基礎を提供
- マルチウインドウの制御
- レンダリング
- 入出力デバイスの管理
など、グラフィカルなUIの基盤となる部分を提供する
X11の構造
クライアントサーバーモデルに基づいている
-
Xサーバー
- ハードウェアに近い部分を抽象化
- ディスプレイへの出力
- キーボードやマウスの入力の受け取り
- など...
- ハードウェアに近い部分を抽象化
-
Xクライアント
- GUIアプリケーションなどの構成要素
これらソフトウェアがXプロトコルを通してやりとりする
画像: https://wayland.freedesktop.org/docs/html/ch03.html より。
以降説明するWMは、この図においてはCompositorにあたる
一方で、Xサーバーにはウインドウの位置関係・前後関係などをよしなに設定してくれる機能は持たない
GUIの実現には、ユーザーの操作をそれらに反映する橋渡し的な存在が必要
そこで WM(ウインドウマネージャ) が用いられる
WM (ウインドウマネージャ)
特別なXクライアントで、主に以下を担当
- ウインドウの位置関係・前後関係の管理
- 受け取った入出力を反映させる
- ウインドウの装飾
- ウインドウのタイトルバーなど
代表的なものとしてi3やawesome、icewmなどがある
基本的にはOS(Windows, MacOSなど)やデスクトップ環境(GNOME, KDEなど)に紐付いているが、Linuxとかでは自由にカスタマイズができるし、もちろん自分で作ることもできる
TinyWMとは
このWMを50行のCコードでミニマムに実装したもの
実装されている機能は以下の3つ:
- ウインドウの移動
- 縦横移動 (掴んで動かす)
- 最前面への移動
- ウインドウの拡大縮小
WM開発への最初のアプローチのひとつ
ソースコードを見てみよう
基本的な流れ
- Xサーバーへの接続
- 受け取る入力イベントの「グラブ」
-
メインループ
- ユーザーの入力の受け取る
- 入力の内容をディスプレイに反映
TinyWMの概観
#include <X11/Xlib.h>
int main(void)
{
((変数の定義))
1. Xサーバーへの接続
2. 入力イベントのグラブ
for(;;) // メインループ
{
3-1. ユーザーの入力の受け取る
3-2. 入力の内容ごとにディスプレイに反映
}
}
1.Xサーバーへの接続
// ((変数の定義))
Display * dpy;
if(!(dpy = XOpenDisplay(0x0))) return 1;
XOpenDisplay
で初めてXサーバーとの通信が確立される
dpy
には、Xサーバーの情報が挿入される (NULLだったら異常終了)
dpy
を他のいろんな関数に引き回しすことで、Xサーバーとのやり取りを行う
Xサーバーとのあらゆるやり取りは実質的にDisplay型のメソッドとして提供されると考えるとわかりやすいかも
2.入力イベントのグラブ
発生する入力イベントは、基本的にウインドウ側で受理される
しかしWM開発においては、入力の情報をウインドウではなくWM側で受け取りたいときがある
(例えば、ウインドウの移動やリサイズなどのGUI操作)
このとき、WM側で入力を受け取るようにするための操作がグラブ
XGrabKey(dpy, XKeysymToKeycode(dpy, XStringToKeysym("F1")), Mod1Mask,
DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync);
XGrabKey
でAlt+F1
をグラブ
ウインドウの最前面移動に使われる
XGrabButton(dpy, 1, Mod1Mask, DefaultRootWindow(dpy), True,
ButtonPressMask|ButtonReleaseMask|PointerMotionMask,
GrabModeAsync, GrabModeAsync, None, None);
XGrabButton(dpy, 3, Mod1Mask, DefaultRootWindow(dpy), True,
ButtonPressMask|ButtonReleaseMask|PointerMotionMask,
GrabModeAsync, GrabModeAsync, None, None);
XGrabButton
でAlt+右/左クリック
をグラブ
ウインドウの移動とリサイズに使われる
3.メインループ
流れとしては
- キューされた入力イベントを一つ取り出す
- 入力イベントをもとにディスプレイ上に反映
を繰り返している
Xサーバーに入力イベントがキューされており、それを毎ループで1つずつ取り出して処理する形
3-1.ユーザーの入力を受け取る
// ((変数の定義))
XEvent ev;
XEventは様々なイベントを表現する共用体
(このあとイベントの種類によってXButtonEvent
などにキャストされる)
XNextEvent(dpy, &ev);
ユーザーの入力の内容は、イベントとしてXサーバー内にキューされる
XNextEvent
でイベントをデキューしev
に格納する
3-2.入力の内容ごとにディスプレイに反映
ev.type
には入力の種類が格納される (KeyPress
やButtonPress
など)
これを元に条件分岐
if(ev.type == ***) // ...キーを押したときの操作だったり...
else if(ev.type == ***) // ... マウスを動かしたときの操作だったり...
else if(ev.type == ***) // ... いろいろ...
入力の種類に応じて、ディスプレイ上のウインドウに対して操作を行う
KeyPress: ウインドウの最前面移動
if(ev.type == KeyPress && ev.xkey.subwindow != None)
XRaiseWindow(dpy, ev.xkey.subwindow);
イベントがKeyPress
であり、かつカーソル上にウインドウがあれば
それを最前面に移動する
本来KeyPress
は他のキーの入力を受け取るときにも使われるが、ここではAlt+F1
しかグラブされていないのでこれだけでOK
ButtonPress: ウインドウの選択
// ((変数の定義))
XWindowAttributes attr;
XButtonEvent start;
else if(ev.type == ButtonPress && ev.xbutton.subwindow != None){
XGetWindowAttributes(dpy, ev.xbutton.subwindow, &attr);
start = ev.xbutton;
}
イベントがButtonPress
であり、かつカーソル上にウインドウがあれば
- ウインドウの情報を
attr
に挿入する - イベントの情報を
start
で持っておく- いずれも、ウインドウの移動やリサイズのときに使う
MotionNotify: ウインドウの移動・リサイズ
else if(ev.type == MotionNotify && start.subwindow != None)
{
int xdiff = ev.xbutton.x_root - start.x_root;
int ydiff = ev.xbutton.y_root - start.y_root;
XMoveResizeWindow(dpy, start.subwindow,
attr.x + (start.button==1 ? xdiff : 0),
attr.y + (start.button==1 ? ydiff : 0),
MAX(1, attr.width + (start.button==3 ? xdiff : 0)),
MAX(1, attr.height + (start.button==3 ? ydiff : 0)));
}
イベントがMotionNotify
であり、かつカーソル上にウインドウがあれば
-
attr
start
内の情報をもとにウインドウを移動・リサイズする- 移動かリサイズかは、
start
内に格納されたボタンの種類で判断
- 移動かリサイズかは、
ButtonRelease: ウインドウの選択の解除
else if(ev.type == ButtonRelease)
start.subwindow = None;
イベントがButtonRelease
であれば、選択を解除する
全部読めた!
やった〜
もっといろんなことをするには
- タイトルバーを実装する
- 実は、X11開発においてはタイトルバー自体も独立したウインドウとして扱われる
- アプリケーションとタイトルバー(などの装飾)をペアとしてうまく管理する実装も必要
- ウインドウの移動やリサイズを、ウインドウの外枠をつまんで行うようにする
- このあたりを実装しようとすると難易度が結構上がる
- 他のWMを読む
- 次のステップとして、例えばlwmは非常に参考になる
https://github.com/jamesfcarter/lwm
- 次のステップとして、例えばlwmは非常に参考になる
- タスクバーやメニュー、ランチャーも作る
- 一般的にWMの責務ではないが、これらも内包するWMのプロジェクトもある
オレオレデスクトップ環境を作ろう
補足事項
- X11は比較的古い技術
- X11は複雑な設計が問題視され、次世代にあたるWaylandに置き換わりつつある
- 新しく作るならWaylandを使うほうがナウい
- 一方で、開発のハードルは未だ高い
- Xサーバーにあたる部分から自力実装する必要がある
- はじめるならまずwlrootsを調べると良い
https://gitlab.freedesktop.org/wlroots/wlroots -
wlrootsを使ったミニマムな実装も存在 (1000行あるよ)
https://gitlab.freedesktop.org/wlroots/wlroots/-/tree/master/tinywl
おわりに
みんなもやろうWM開発
素晴らしいプロジェクトを作り上げたmackstann氏に感謝。
参考文献
Discussion