✖️

読むWM "TinyWM" をちゃんと読む - X11

2023/09/30に公開

この記事について

https://slides.peruki.dev/slides/2023/LT/TOGATTA/vol4/

この記事は、TOGATTA SERVER LT Vol.4にて発表したスライドをZenn向けに公開したものです。

Marpで書かれたスライドの原稿がベースなので、書いている文章がスライドっぽかったりします。ご了承ください。


概要

https://github.com/mackstann/tinywm

「WM(ウインドウマネージャ)」 のミニマルな実装であるTinyWMを読んで、X11におけるWMの振る舞いを理解しよう

前提知識

X11 (X Window System) とは

UnixおよびLinux系OSでよく使われるウィンドウシステムの一つ

  • GUI環境の基礎を提供
    • マルチウインドウの制御
    • レンダリング
  • 入出力デバイスの管理

など、グラフィカルなUIの基盤となる部分を提供する

https://ja.wikipedia.org/wiki/ウィンドウシステム

X11の構造

クライアントサーバーモデルに基づいている

  • Xサーバー
    • ハードウェアに近い部分を抽象化
      • ディスプレイへの出力
      • キーボードやマウスの入力の受け取り
      • など...
  • Xクライアント
    • GUIアプリケーションなどの構成要素

これらソフトウェアがXプロトコルを通してやりとりする


画像: https://wayland.freedesktop.org/docs/html/ch03.html より。
以降説明するWMは、この図においてはCompositorにあたる

一方で、Xサーバーにはウインドウの位置関係・前後関係などをよしなに設定してくれる機能は持たない

GUIの実現には、ユーザーの操作をそれらに反映する橋渡し的な存在が必要
そこで WM(ウインドウマネージャ) が用いられる

WM (ウインドウマネージャ)

特別なXクライアントで、主に以下を担当

  • ウインドウの位置関係・前後関係の管理
    • 受け取った入出力を反映させる
  • ウインドウの装飾
    • ウインドウのタイトルバーなど

代表的なものとしてi3awesomeicewmなどがある

基本的にはOS(Windows, MacOSなど)やデスクトップ環境(GNOME, KDEなど)に紐付いているが、Linuxとかでは自由にカスタマイズができるし、もちろん自分で作ることもできる

TinyWMとは

https://github.com/mackstann/tinywm

このWMを50行のCコードでミニマムに実装したもの

実装されている機能は以下の3つ:

  • ウインドウの移動
    • 縦横移動 (掴んで動かす)
    • 最前面への移動
  • ウインドウの拡大縮小

WM開発への最初のアプローチのひとつ

ソースコードを見てみよう

https://github.com/mackstann/tinywm/blob/master/tinywm.c

基本的な流れ

  1. Xサーバーへの接続
  2. 受け取る入力イベントの「グラブ」
  3. メインループ
    • ユーザーの入力の受け取る
    • 入力の内容をディスプレイに反映

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);

XGrabKeyAlt+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);

XGrabButtonAlt+右/左クリックをグラブ
ウインドウの移動とリサイズに使われる

3.メインループ

流れとしては

  • キューされた入力イベントを一つ取り出す
  • 入力イベントをもとにディスプレイ上に反映

を繰り返している

Xサーバーに入力イベントがキューされており、それを毎ループで1つずつ取り出して処理する形

3-1.ユーザーの入力を受け取る

// ((変数の定義))
XEvent ev;

XEventは様々なイベントを表現する共用体
(このあとイベントの種類によってXButtonEventなどにキャストされる)

    XNextEvent(dpy, &ev);

ユーザーの入力の内容は、イベントとしてXサーバー内にキューされる
XNextEventでイベントをデキューしevに格納する

3-2.入力の内容ごとにディスプレイに反映

ev.typeには入力の種類が格納される (KeyPressButtonPressなど)

これを元に条件分岐

    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を読む
  • タスクバーやメニュー、ランチャーも作る
    • 一般的にWMの責務ではないが、これらも内包するWMのプロジェクトもある

オレオレデスクトップ環境を作ろう

補足事項

おわりに

みんなもやろうWM開発

https://github.com/mackstann/tinywm

素晴らしいプロジェクトを作り上げたmackstann氏に感謝。

参考文献

https://qiita.com/ai56go/items/1b8bfeede2b467ac0667

https://qiita.com/ai56go/items/dec1307f634181d923f5

https://zenn.dev/hidenori3/articles/c2be2bd50fc8dd

https://ja.wikipedia.org/wiki/ウィンドウシステム

https://wayland.freedesktop.org/

Discussion