emacsのtooltip作成用のライブラリのposframe.elの使いかたを調べていた。

経緯

元々の経緯として、補完のinterfaceにivyを検討していたのだけれど、どうも候補選択のUIは現在のカーソル位置からのtooltipのような形で出て欲しいかなと思ったりしていた。

現状はcompany-modeを使っていたのだけれど。ivyでできないかなーと思ったりしていた。その過程でivy-posframeを知った。

このivy-posframeが内部で利用しているライブラリがposframe

github.com github.com

posframe?

posframeはtooltip的なものなどを作成するために使えそうなUIライブラリ的なもの。ちょっと風変わりだと思ったのはchild-frame(おそらくchild processに倣ってこの呼び方)を作成してそれを提供する点。

Posframe can pop a posframe at point, this posframe is a child-frame with its root window's buffer.

frame? window?

その前にemacsでのframeが何かということについて説明を書いておく。

  • window -- 1frame内の表示領域のこと
  • frame -- windowを持った画面のこと。一般的なアプリケーションでのウィンドウがemacsでのframeに対応している。

例えば、C-x 2C-x 3で1つのアプリケーションウィンドウを水平や垂直に分割したときに、分割された個々の表示領域のことはwindowと呼ぶ。

一方frameはM-x new-frameでアプリケーションウィンドウを新しく開いたりしたときに生成されたそのウィンドウ(パネル)を意味するもののことを指している。

絵にすると以下の様な感じ。下の図は2つのframeを表示したときのこと。

frame 1

--------------------
|window 1          |
--------------------
|window 2| window 3|
--------------------
|window 4          |
--------------------

frame 2
--------------------
|        |         |
|window 1| window 2|
|        |         |
--------------------

詳しくはこのあたりを読む

elisp的なframeの扱い

画面上の背景や文字色等はframeのparameterとして格納されている。現在アクティブなframeはselected-frameを使って取れる。

(selected-frame)
;; => #<frame emacs@localhost 0x13bfc60>

(frame-parameter (selected-frame) 'foreground-color)
;; => "#f8f8f2"

ちなみにnilを渡した時にも、現在アクティブなframeな情報を返してくれる。一気に全てのparametersを取りたいときには代わりにframe-parametersを使う。

(frame-parameter nil 'background-color)
;; => "#282a36"

そうそう、良い機会だったのでついでにemacsのcolor-themeをdracula-themeに変更した(doom-draculaは使っていない)。先程の例の背景色などはdracula-themeのもの。

draculatheme.com

frameがwindowを所有するので、frame-listで取り出したframeが所持するwindowをwindow-listで取り出せる。

(frame-list)
;; => (#<frame *scratch* 0x6998890> #<frame *scratch* 0x13bfc60>)

(window-list (first (frame-list)))
;; => (#<window 207 on *scratch*>)
(window-list (second (frame-list)))
;; => (#<window 118 on *scratch*> #<window 212 on posframe.el>)

windowの削除などは、おそらくふつうにemacsを使っているなら息を吸う様に使っているであろうC-x 0C-x 1にbindされている(delete-windows, delete-other-windows)。

frameの削除はdelete-framedelete-other-frameを使う。delete-other-frameC-x 5 1という馴染みのないキーにbindされているらしい。

frameを隠すこととframeを削除すること

先程、frameの削除については紹介したがもう一つframeに対する操作がある。それは表示・非表示にすること。このあたりはmake-frame-invisibleなどでできる

;; 引数を省略しても良い
(make-frame-invisible (selected-frame))

詳しくはこのあたり

ようやくframe,windowの説明が終わった。

posframe

posframeは先程説明したframeを使ってtooltip的な機能を表現したモノのこと。とりあえずposframe-showを覚えれば良い。

渡したbufferの内容をtooltipとして表示する。なので、複数の場所に表示したい場合には渡すbufferを変える必要がある。引数としてバッファ名を渡すこともできる。

;; need:  package-install posframe
(require 'posframe)

(posframe-show "*sample buffer*" :string "hello")

バッファ名を文字列で渡し、その内容を":string"オプションで指定した例。

posframe sample1

ちょっとtooltipっぽくないかもしれない。borderを追加してみる。

postframe sample2

作られたchild-frameはstickyで付いてくる(ディスプレイ上の座標を指定しての表示なのでそれはそう)。

tooltipを消したい

tooltip(child-frame)は消されるまでずっと表示され続ける。消す方法を調べる必要がありそう。

tooltipを全部消したい場合には posframe-delete-allを使えば良い。

ちなみにchild-frameかどうかは、そのframeがpostframe-bufferというparameterを持っているかどうかで確認できる。

(cl-loop
 for f in (frame-list)
 when (frame-parameter f 'posframe-buffer)
 collect f
 );; => (#<frame  0x4cc9030> #<frame  0x9414f50>)

個別に消したい場合には、以下の様なhookが追加されているので、対応するbufferをただkillするだけでも消せるといえば消せる。

(defun posframe-auto-delete ()
  "Auto delete posframe when its buffer is killed.

This function is used by `kill-buffer-hook'."
  (posframe-delete-frame (current-buffer)))

(add-hook 'kill-buffer-hook #'posframe-auto-delete)

そんなわけでtimerなどと合わせれば以下の様な形でN秒後にtooltipを消すということもできる。

;; 3秒後に消える
(lexical-let ((bufname "*me*"))
  (posframe-show
   bufname
   :internal-border-width 20
   :string "hello!!"
   )
  (run-at-time "3 sec" nil
               #'(lambda ()
                   (kill-buffer (get-buffer bufname))
                   ))
  )

ただちょっと待って欲しい。

消すことと非表示にすること

この記事の途中でframeの削除とは別にframeの非表示についても説明していた。ふつうに考えてtooltipを表示するたびに毎回frameを作ったり消したりしているのでは効率が悪いかもしれない。

幸いposframe-show:timeoutというオプションを持っており、これを使えばN秒後の非表示を実現できる。

;; 3秒後に非表示にする(隠す)
(lexical-let ((bufname "*me*"))
  (posframe-show
   bufname
   :internal-border-width 20
   :string "hello!!"
   :timeout 3
   )
  )

裏側では先程説明したmake-frame-invisibleなどが使われている。

その他対応しているオプション

その他色々なオプションが指定できるので後で調整したい場合にはこの辺のオプションの挙動などを調べることになりそう(describe-functionなどで調べていく感じ)。

(cl-defun posframe-show (posframe-buffer
                         &key
                         string
                         position
                         poshandler
                         width
                         height
                         min-width
                         min-height
                         x-pixel-offset
                         y-pixel-offset
                         left-fringe
                         right-fringe
                         internal-border-width
                         internal-border-color
                         font
                         foreground-color
                         background-color
                         respect-header-line
                         respect-mode-line
                         face-remap
                         initialize
                         no-properties
                         keep-ratio
                         override-parameters
                         timeout
                         refresh
...

色の選択(ivyとivy-posframeとの表示の違い)

ちなみにposframe-showに渡す色の選択には、冒頭のivyファミリー(?)のcounselcounsel-colors-emacsがそれなりに便利だった。

(counsel-colors-emacs)

こちらは通常のivyの表示

ivy

一時的にivy-posframeを使う(colors-emacsではなくcolors-webというものもある)。

(let ((ivy-display-function #'ivy-posframe-display-at-point))
  (counsel-colors-web))

ivy

(他にもivy-posframe-displayで始まるtooltipの表示位置の計算が異なる関数が色々ある)

;; (apropos "ivy-posframe-display-")

ivy-posframe-display-at-frame-bottom-left
  Function: (not documented)
ivy-posframe-display-at-frame-bottom-window-center
  Function: (not documented)
ivy-posframe-display-at-frame-center
  Function: (not documented)
ivy-posframe-display-at-point
  Function: (not documented)
ivy-posframe-display-at-window-bottom-left
  Function: (not documented)
ivy-posframe-display-at-window-center
  Function: (not documented)

まじめに設定する場合には以下の様な形(再起動以外に設定を取り除く方法がないあたり微妙ではある)。

(require 'ivy)
(require 'ivy-posframe)

(ivy-mode 1)
(add-to-list 'ivy-display-functions-alist '(complete-symbol . ivy-posframe-display-at-point))
(ivy-posframe-enable)

ivy-display-functionに特定の関数を1つだけ設定する方法もあるけれど、正直それは微妙だと思う。けっこう状況によってtooltip的に表示されてほしいかminibuffer的に表示されてほしいか変わる気がするし。

posframeの使いかた

posframeによるtooltipはけっこう手軽で良さそうだと思ったのだけれど。まじめにUIを作ろうと思った時に結局発生する座標管理がだるいなーと思ったし。それを何らかのdocumentでという風になったらDOMの再発明だなーという感じになったりした。

とはいえminibufferに表示されるよりtooltip的な見た目の方が見やすいことはたしかで、ちょっとしたインジケーター的なものとかposframeで作ってみたりしても良いのかもしれないと思ったりした。

そんなわけでなるべくposframeの仕組み自体が後でわかるように記事にしてみた。