高橋メソッドなプレゼンツール on wxHaskell

wxHaskellを使って、高橋メソッドなプレゼンツールを作ってみました。Haskellでプレゼンツールってあんまりないですよね?
ダウンロード: Windows用実行形式 + ソースコード
チュートリアル:

  • アーカイブ中のsample.batを実行してください。サンプルのプレゼンテーションが始まります。
  • プレゼンテーションの書き方については、sample.txtを参考にしてください。

操作方法:

  • テキストファイルをwxtm.exeにドラッグ & ドロップします。
  • お好みに応じて画面を最大化します。
  • キーボードを使ってページをめくります。キーバインドはlessです。以上。

BUGS:

  • マウスは使えません。
  • フォントのサイズの計算アルゴリズムがいまいちで、描画が遅れる場合があります。高橋メソッドはテンポ命なのに…

参照: 高橋メソッド
以下、ソースです。相変らずIOの山です。うーむ。GUIでIOとそれ以外を上手く分離するにはどうしたらいいんだろう。
ファイル: wxtm.hs

module Main (main) where

import Control.Monad as M (when)
import Graphics.UI.WX
import Graphics.UI.WXCore
import System (getArgs)
import System.IO (readFile)

type Page = [String]

main :: IO ()
main = start . gui . parse =<< readFiles =<< getArgs

readFiles :: [String] -> IO String
readFiles [] = error "set files."
readFiles files = return . concat =<< mapM readFile files

parse :: String -> [Page]
parse s = parse' $ lines s
  where
    parse' :: [String] -> [Page]
    parse' [] = [[]]
    parse' ([] : ss) = [] : parse' ss
    parse' (s : ss) =
      let (p : ps) = parse' ss
      in ((s : p) : ps)

gui :: [Page] -> IO ()
gui pages = do
  vpageno <- varCreate (0)
  f <- frame [text     := "wxtm", clientSize := sz 640 480]
  set f [on paint      := onPaint      f vpageno pages,
         on anyKey     := onKey        f vpageno pages]

onPaint :: Frame () -> Var Int -> [Page] -> DC () -> Rect -> IO ()
onPaint f vpageno pages dc view = do
  set dc [fontFace := "MS", fontWeight := WeightBold,
          penColor := white, brushColor := white]
  csz    <- windowGetClientSize f
  pageno <- varGet vpageno
  drawPage csz (pages !! pageno)
  set f [text  := "wxtm - page:" ++ (show (pageno + 1))]  
  where
    drawPage :: Size -> Page -> IO ()
    drawPage csz page = do
      drawRect dc view []
      sequence_ $ zipWith (drawLine csz (length page)) [0 ..] page

    drawLine :: Size -> Int -> Int -> String -> IO ()
    drawLine csz nlines lineno line = do
      let margin = 5 -- %
          cw = sizeW csz
          ch = sizeH csz `div` nlines
          w  = cw * (100 - margin) `div` 100
          h  = ch * (100 - margin) `div` 100
      tsz <- setFontSize (sz w h) h (h `div` 20) line
      let tw = sizeW tsz
          th = sizeH tsz
          x  = (cw - tw) `div` 2
          y  = (lineno * ch) + ((ch - th) `div` 2)
      drawText dc line (Point x y) []

    setFontSize :: Size -> Int -> Int -> String -> IO Size
    setFontSize sz fontSz fontStep s = do
      set dc [fontSize := fontSz]
      tsz <- getTextExtent dc s
      if ((sizeW tsz > sizeW sz) || (sizeH tsz > sizeH sz))
        then setFontSize sz (fontSz - fontStep) fontStep s
        else return tsz

onKey :: Frame () -> Var Int -> [Page] -> Key -> IO ()
onKey f vpageno pages key = do
  case key of
    KeyEscape   -> close f
    KeyChar 'q' -> close f
    KeyReturn   -> nextPage
    KeySpace    -> nextPage
    KeyBack     -> prevPage
    KeyDelete   -> prevPage
    KeyChar 'b' -> prevPage
    KeyChar 'p' -> firstPage
    KeyChar 'G' -> lastPage
    KeyPrior    -> prevPage
    KeyNext     -> nextPage
    KeyHome     -> firstPage
    KeyEnd      -> lastPage
    otherwise   -> return ()
  where
    firstPage :: IO ()
    firstPage = varSet vpageno 0 >> repaint f
    
    lastPage :: IO ()
    lastPage = varSet vpageno (length pages - 1) >> repaint f
    
    nextPage :: IO ()
    nextPage = do
      pageno <- varGet vpageno
      M.when (pageno < length pages - 1) $ varSet vpageno (pageno + 1)
      repaint f
    
    prevPage :: IO ()
    prevPage = do
      pageno <- varGet vpageno
      M.when (pageno > 0) $ varSet vpageno (pageno - 1)
      repaint f

-- Local Variables:
-- compile-command: "ghc -W -fno-warn-unused-matches --make -o wxtm wxtm"
-- End:

ファイル: Makefile

TARGET = wxtm

HC          = ghc
HCFLAGS     = -W -fno-warn-unused-matches --make
HCFLAGS_REL = -optl-mwindows
UPX         = upx
UPXFLAGS    = --overlay=strip --strip-relocs=0
EXE         = .exe

all: $(TARGET)$(EXE)

$(TARGET)$(EXE): $(TARGET).hs
	$(HC) $(HCFLAGS) $(HCFLAGS_REL) $< -o $@
	$(UPX) $(UPXFLAGS) $@

clean:
	rm -f $(TARGET)_d$(EXE) $(TARGET)$(EXE) *.o *.hi