module Game.LambdaHack.Client.UI.Frontend.Gtk
(
FrontendSession(sescMVar)
, fdisplay, fpromptGetKey, fsyncFrames
, frontendName, startup
) where
import Control.Applicative
import Control.Concurrent
import Control.Concurrent.Async
import qualified Control.Concurrent.STM as STM
import qualified Control.Exception as Ex hiding (handle)
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
import Game.LambdaHack.Common.Point
data FrameState =
FPushed
{ fpushed :: !(LQueue (Maybe GtkFrame))
, fshown :: !GtkFrame
}
| FNone
data FrontendSession = FrontendSession
{ sview :: !TextView
, stags :: !(M.Map Color.Attr TextTag)
, schanKey :: !(STM.TQueue 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, ..}
FNone ->
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 <- M.fromList <$>
mapM (\ ak -> do
tt <- textTagNew Nothing
textTagTableAdd ttt tt
doAttr sdebugCli 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 <- STM.atomically STM.newTQueue
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
let flushChanKey = do
res <- STM.atomically $ STM.tryReadTQueue schanKey
when (isJust res) flushChanKey
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 = let md = modifierTranslate mods
in if md == K.Shift then K.NoModifier else md
!pointer = Nothing
liftIO $ do
unless (deadKey n) $ do
when (key == K.Esc) $ do
void $ tryPutMVar escMVar ()
flushChanKey
STM.atomically $ STM.writeTQueue schanKey K.KM{..}
return True
f <- fontDescriptionFromString $ fromMaybe "" sfont
widgetModifyFont sview (Just f)
liftIO $ do
textViewSetLeftMargin sview 3
textViewSetRightMargin sview 3
currentfont <- newIORef f
Just display <- displayGetDefault
cursor <- cursorNewForDisplay display Tcross
sview `on` buttonPressEvent $ do
liftIO flushChanKey
but <- eventButton
(wx, wy) <- eventCoordinates
mods <- eventModifier
let !modifier = modifierTranslate mods
liftIO $ do
when (but == RightButton && modifier == K.Control) $ 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
hasSelection <- textBufferHasSelection tb
unless hasSelection $ do
mdrawWin <- displayGetWindowAtPointer display
let setCursor (drawWin, _, _) =
drawWindowSetCursor drawWin (Just cursor)
maybe (return ()) setCursor mdrawWin
(bx, by) <-
textViewWindowToBufferCoords sview TextWindowText
(round wx, round wy)
(iter, _) <- textViewGetIterAtPosition sview bx by
cx <- textIterGetLineOffset iter
cy <- textIterGetLine iter
let !key = case but of
LeftButton -> K.LeftButtonPress
MiddleButton -> K.MiddleButtonPress
RightButton -> K.RightButtonPress
_ -> K.LeftButtonPress
!pointer = Just $! Point cx (cy 1)
STM.atomically $ STM.writeTQueue schanKey K.KM{..}
return $! but == RightButton
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 = 30
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
FNone -> do
putMVar sframeState fs
threadDelay $ microInSec `div` (maxFps * 2)
pollFramesAct sess
pushFrame :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()
pushFrame sess 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 && isJust rawFrame
then fs
else FPushed{fpushed = writeLQueue fpushed nextFrame, ..}
FNone | immediate -> do
maybe (return ()) (postGUIAsync . output sess) nextFrame
putMVar sframeState FNone
FNone ->
putMVar sframeState
$ if isNothing nextFrame && anyFollowed && isJust rawFrame
then fs
else FPushed{ fpushed = writeLQueue newLQueue nextFrame
, fshown = dummyFrame }
case nextFrame of
Nothing -> putMVar slastFull (lastFrame, not (case fs of
FNone -> True
FPushed{} -> False
&& immediate
&& not anyFollowed))
Just f -> putMVar slastFull (f, False)
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 (return ()) (postGUIAsync . output sess) nextFrame
Nothing -> return ()
FNone -> return ()
putMVar sframeState FNone
fdisplay :: FrontendSession
-> Maybe SingleFrame
-> IO ()
fdisplay sess = pushFrame sess False
displayAllFramesSync :: FrontendSession -> FrameState -> IO ()
displayAllFramesSync sess@FrontendSession{sdebugCli=DebugModeCli{..}, sescMVar}
fs = do
escPressed <- case sescMVar of
Nothing -> return False
Just escMVar -> not <$> isEmptyMVar escMVar
let maxFps = fromMaybe defaultMaxFps smaxFps
case fs of
_ | escPressed -> return ()
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 ()
FNone ->
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 $ Just frame
km <- STM.atomically $ STM.readTQueue 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
| Control `elem` mods = K.Control
| any (`elem` mods) [Meta, Super, Alt, Alt2, Alt3, Alt4, Alt5] = K.Alt
| Shift `elem` mods = K.Shift
| otherwise = K.NoModifier
doAttr :: DebugModeCli -> TextTag -> Color.Attr -> IO ()
doAttr sdebugCli tt attr@Color.Attr{fg, bg}
| attr == Color.defAttr = return ()
| fg == Color.defFG =
set tt $ extraAttr sdebugCli
++ [textTagBackground := Color.colorToRGB bg]
| bg == Color.defBG =
set tt $ extraAttr sdebugCli
++ [textTagForeground := Color.colorToRGB fg]
| otherwise =
set tt $ extraAttr sdebugCli
++ [ textTagForeground := Color.colorToRGB fg
, textTagBackground := Color.colorToRGB bg ]
extraAttr :: DebugModeCli -> [AttrOp TextTag]
extraAttr DebugModeCli{scolorIsBold} =
[textTagWeight := fromEnum WeightBold | scolorIsBold == Just True]