module Game.LambdaHack.Client.UI.Frontend.Gtk
(
FrontendSession(sescMVar)
, fdisplay, fpromptGetKey, fsyncFrames
, frontendName, startup
) where
import Control.Concurrent
import Control.Concurrent.Async
import qualified Control.Exception as Ex hiding (handle)
import Control.Exception.Assert.Sugar
import Control.Monad
import Control.Monad.Reader
import qualified Data.ByteString.Char8 as BS
import Data.IORef
import Data.List
import qualified Data.Map.Strict as M
import Data.Maybe
import Data.String (IsString (..))
import qualified Data.Text as T
import Graphics.UI.Gtk hiding (Point)
import System.Time
import qualified Game.LambdaHack.Client.Key as K
import Game.LambdaHack.Client.UI.Animation
import Game.LambdaHack.Common.ClientOptions
import qualified Game.LambdaHack.Common.Color as Color
import Game.LambdaHack.Common.LQueue
data FrameState =
FPushed
{ fpushed :: !(LQueue (Maybe GtkFrame))
, fshown :: !GtkFrame
}
| FNone
data FrontendSession = FrontendSession
{ sview :: !TextView
, stags :: !(M.Map Color.Attr TextTag)
, schanKey :: !(Chan K.KM)
, sframeState :: !(MVar FrameState)
, slastFull :: !(MVar (GtkFrame, Bool))
, sescMVar :: !(Maybe (MVar ()))
, sdebugCli :: !DebugModeCli
}
data GtkFrame = GtkFrame
{ gfChar :: !BS.ByteString
, gfAttr :: ![[TextTag]]
}
deriving Eq
dummyFrame :: GtkFrame
dummyFrame = GtkFrame BS.empty []
onQueue :: (LQueue (Maybe GtkFrame) -> LQueue (Maybe GtkFrame))
-> FrontendSession -> IO ()
onQueue f FrontendSession{sframeState} = do
fs <- takeMVar sframeState
case fs of
FPushed{..} ->
putMVar sframeState FPushed{fpushed = f fpushed, ..}
_ ->
putMVar sframeState fs
frontendName :: String
frontendName = "gtk"
startup :: DebugModeCli -> (FrontendSession -> IO ()) -> IO ()
startup = runGtk
runGtk :: DebugModeCli -> (FrontendSession -> IO ()) -> IO ()
runGtk sdebugCli@DebugModeCli{sfont} cont = do
unsafeInitGUIForThreadedRTS
ttt <- textTagTableNew
stags <- fmap M.fromList $
mapM (\ ak -> do
tt <- textTagNew Nothing
textTagTableAdd ttt tt
doAttr tt ak
return (ak, tt))
[ Color.Attr{fg, bg}
| fg <- [minBound..maxBound], bg <- Color.legalBG ]
tb <- textBufferNew (Just ttt)
sview <- textViewNewWithBuffer tb
textViewSetEditable sview False
textViewSetCursorVisible sview False
schanKey <- newChan
let frameState = FNone
sframeState <- newMVar frameState
slastFull <- newMVar (dummyFrame, False)
escMVar <- newEmptyMVar
let sess = FrontendSession{sescMVar = Just escMVar, ..}
aCont <- async $ cont sess `Ex.finally` postGUISync mainQuit
link aCont
aPoll <- async $ pollFramesAct sess `Ex.finally` postGUISync mainQuit
link aPoll
sview `on` keyPressEvent $ do
n <- eventKeyName
mods <- eventModifier
#if MIN_VERSION_gtk(0,13,0)
let !key = K.keyTranslate $ T.unpack n
#else
let !key = K.keyTranslate n
#endif
!modifier = modifierTranslate mods
liftIO $ do
unless (deadKey n) $ do
when (key == K.Esc) $
void $ tryPutMVar escMVar ()
writeChan schanKey K.KM{key, modifier}
return True
f <- fontDescriptionFromString $ fromMaybe "" sfont
widgetModifyFont sview (Just f)
currentfont <- newIORef f
sview `on` buttonPressEvent $ do
but <- eventButton
liftIO $ case but of
RightButton -> do
fsd <- fontSelectionDialogNew ("Choose font" :: String)
cf <- readIORef currentfont
fds <- fontDescriptionToString cf
fontSelectionDialogSetFontName fsd (fds :: String)
fontSelectionDialogSetPreviewText fsd ("eee...@.##+##" :: String)
resp <- dialogRun fsd
when (resp == ResponseOk) $ do
fn <- fontSelectionDialogGetFontName fsd
case fn :: Maybe String of
Just fn' -> do
fd <- fontDescriptionFromString fn'
writeIORef currentfont fd
widgetModifyFont sview (Just fd)
Nothing -> return ()
widgetDestroy fsd
return True
_ -> return False
let black = Color minBound minBound minBound
white = Color 0xC500 0xBC00 0xB800
widgetModifyBase sview StateNormal black
widgetModifyText sview StateNormal white
w <- windowNew
containerAdd w sview
onDestroy w mainQuit
widgetShowAll w
mainGUI
output :: FrontendSession
-> GtkFrame
-> IO ()
output FrontendSession{sview, stags} GtkFrame{..} = do
tb <- textViewGetBuffer sview
let attrs = zip [0..] gfAttr
defAttr = stags M.! Color.defAttr
textBufferSetByteString tb gfChar
mapM_ (setTo tb defAttr 0) attrs
setTo :: TextBuffer -> TextTag -> Int -> (Int, [TextTag]) -> IO ()
setTo _ _ _ (_, []) = return ()
setTo tb defAttr lx (ly, attr:attrs) = do
ib <- textBufferGetIterAtLineOffset tb ly lx
ie <- textIterCopy ib
let setIter :: TextTag -> Int -> [TextTag] -> IO ()
setIter previous repetitions [] = do
textIterForwardChars ie repetitions
when (previous /= defAttr) $
textBufferApplyTag tb previous ib ie
setIter previous repetitions (a:as)
| a == previous =
setIter a (repetitions + 1) as
| otherwise = do
textIterForwardChars ie repetitions
when (previous /= defAttr) $
textBufferApplyTag tb previous ib ie
textIterForwardChars ib repetitions
setIter a 1 as
setIter attr 1 attrs
maxPolls :: Int -> Int
maxPolls maxFps = max 120 (2 * maxFps)
picoInMicro :: Int
picoInMicro = 1000000
addTime :: ClockTime -> Int -> ClockTime
addTime (TOD s p) mus = TOD s (p + fromIntegral (mus * picoInMicro))
diffTime :: ClockTime -> ClockTime -> Int
diffTime (TOD s1 p1) (TOD s2 p2) =
fromIntegral (s1 s2) * picoInMicro +
fromIntegral (p1 p2) `div` picoInMicro
microInSec :: Int
microInSec = 1000000
defaultMaxFps :: Int
defaultMaxFps = 15
pollFramesWait :: FrontendSession -> ClockTime -> IO ()
pollFramesWait sess@FrontendSession{sdebugCli=DebugModeCli{smaxFps}}
setTime = do
let maxFps = fromMaybe defaultMaxFps smaxFps
curTime <- getClockTime
let diffSetCur = diffTime setTime curTime
if diffSetCur > microInSec `div` maxPolls maxFps
then do
threadDelay $ diffTime curTime setTime `div` 2
pollFramesWait sess setTime
else
pollFramesAct sess
pollFramesAct :: FrontendSession -> IO ()
pollFramesAct sess@FrontendSession{sframeState, sdebugCli=DebugModeCli{..}} = do
let maxFps = fromMaybe defaultMaxFps smaxFps
fs <- takeMVar sframeState
case fs of
FPushed{..} ->
case tryReadLQueue fpushed of
Just (Just frame, queue) -> do
putMVar sframeState FPushed{fpushed = queue, fshown = frame}
curTime <- getClockTime
postGUISync $ output sess frame
threadDelay $ microInSec `div` (maxFps * 2)
pollFramesWait sess $ addTime curTime $ microInSec `div` maxFps
Just (Nothing, queue) -> do
putMVar sframeState FPushed{fpushed = queue, ..}
unless snoDelay $
threadDelay $ microInSec `div` maxFps
pollFramesAct sess
Nothing -> do
putMVar sframeState fs
threadDelay $ microInSec `div` maxPolls maxFps
pollFramesAct sess
_ -> do
putMVar sframeState fs
threadDelay $ microInSec `div` (maxFps * 2)
pollFramesAct sess
pushFrame :: FrontendSession -> Bool -> Bool -> Maybe SingleFrame -> IO ()
pushFrame sess noDelay immediate rawFrame = do
let FrontendSession{sframeState, slastFull} = sess
let !frame = case rawFrame of
Nothing -> Nothing
Just fr -> Just $! evalFrame sess fr
(lastFrame, anyFollowed) <- takeMVar slastFull
let nextFrame = if frame == Just lastFrame
then Nothing
else frame
fs <- takeMVar sframeState
case fs of
FPushed{..} ->
putMVar sframeState
$ if isNothing nextFrame && anyFollowed
then fs
else FPushed{fpushed = writeLQueue fpushed nextFrame, ..}
FNone | immediate -> do
maybe skip (postGUIAsync . output sess) nextFrame
putMVar sframeState FNone
FNone ->
let fpushed = if isJust nextFrame
then writeLQueue newLQueue nextFrame
else newLQueue
fshown = dummyFrame
in putMVar sframeState FPushed{..}
case nextFrame of
Nothing -> putMVar slastFull (lastFrame, True)
Just f -> putMVar slastFull (f, noDelay)
evalFrame :: FrontendSession -> SingleFrame -> GtkFrame
evalFrame FrontendSession{stags} rawSF =
let SingleFrame{sfLevel} = overlayOverlay rawSF
sfLevelDecoded = map decodeLine sfLevel
levelChar = unlines $ map (map Color.acChar) sfLevelDecoded
gfChar = BS.pack $ init levelChar
gfAttr = reverse $ foldl' ff [] sfLevelDecoded
ff ll l = reverse (foldl' f [] l) : ll
f l ac = let !tag = stags M.! Color.acAttr ac in tag : l
in GtkFrame{..}
trimFrameState :: FrontendSession -> IO ()
trimFrameState sess@FrontendSession{sframeState} = do
fs <- takeMVar sframeState
case fs of
FPushed{..} ->
case lastLQueue fpushed of
Just frame -> do
let lastFrame = fshown
nextFrame = if frame == lastFrame
then Nothing
else Just frame
maybe skip (postGUIAsync . output sess) nextFrame
Nothing -> return ()
FNone -> return ()
putMVar sframeState FNone
fdisplay :: FrontendSession
-> Bool
-> Maybe SingleFrame
-> IO ()
fdisplay sess noDelay = pushFrame sess noDelay False
displayAllFramesSync :: FrontendSession -> FrameState -> IO ()
displayAllFramesSync sess@FrontendSession{sdebugCli=DebugModeCli{..}} fs = do
let maxFps = fromMaybe defaultMaxFps smaxFps
case fs of
FPushed{..} ->
case tryReadLQueue fpushed of
Just (Just frame, queue) -> do
postGUISync $ output sess frame
threadDelay $ microInSec `div` maxFps
displayAllFramesSync sess FPushed{fpushed = queue, fshown = frame}
Just (Nothing, queue) -> do
unless snoDelay $
threadDelay $ microInSec `div` maxFps
displayAllFramesSync sess FPushed{fpushed = queue, ..}
Nothing ->
return ()
_ ->
return ()
fsyncFrames :: FrontendSession -> IO ()
fsyncFrames sess@FrontendSession{sframeState} = do
fs <- takeMVar sframeState
displayAllFramesSync sess fs
putMVar sframeState FNone
fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM
fpromptGetKey sess@FrontendSession{..}
frame = do
pushFrame sess True True $ Just frame
km <- readChan schanKey
case km of
K.KM{key=K.Space} ->
onQueue dropStartLQueue sess
_ ->
trimFrameState sess
return km
deadKey :: (Eq t, IsString t) => t -> Bool
deadKey x = case x of
"Shift_L" -> True
"Shift_R" -> True
"Control_L" -> True
"Control_R" -> True
"Super_L" -> True
"Super_R" -> True
"Menu" -> True
"Alt_L" -> True
"Alt_R" -> True
"ISO_Level2_Shift" -> True
"ISO_Level3_Shift" -> True
"ISO_Level2_Latch" -> True
"ISO_Level3_Latch" -> True
"Num_Lock" -> True
"Caps_Lock" -> True
_ -> False
modifierTranslate :: [Modifier] -> K.Modifier
modifierTranslate mods =
if Control `elem` mods then K.Control else K.NoModifier
doAttr :: TextTag -> Color.Attr -> IO ()
doAttr tt attr@Color.Attr{fg, bg}
| attr == Color.defAttr = return ()
| fg == Color.defFG = set tt [textTagBackground := Color.colorToRGB bg]
| bg == Color.defBG = set tt [textTagForeground := Color.colorToRGB fg]
| otherwise = set tt [textTagForeground := Color.colorToRGB fg,
textTagBackground := Color.colorToRGB bg]