packages feed

intricacy-0.3.1: SDLUIMInstance.hs

-- This file is part of Intricacy
-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

{-# LANGUAGE FlexibleInstances #-}
module SDLUIMInstance () where

import Graphics.UI.SDL hiding (flip)
import qualified Graphics.UI.SDL as SDL
import qualified Graphics.UI.SDL.TTF as TTF
import Control.Concurrent.STM
import Control.Applicative hiding ((<*>))
import qualified Data.Map as Map
import Data.Map (Map)
import Data.Maybe
import Control.Concurrent (threadDelay)
import Control.Monad.State
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader
import Data.Word
import Data.Array
import Data.List
import Data.Function (on)
import Data.Foldable (for_)
--import Debug.Trace (traceShow)

import MainState
import Hex
import Command
import GameStateTypes
import Lock
import KeyBindings
import Mundanities
import Metagame
import Protocol
import Cache
import Database
import ServerAddr
import Util
import InputMode
import SDLRender
import SDLUI

instance UIMonad (StateT UIState IO) where
    runUI m = evalStateT m nullUIState
    drawMainState = do
	lift $ clearButtons >> clearSelectables
	s <- get
	let mode = ms2im s
	lift $ waitFrame
	drawMainState' s
	lift . drawTitle =<< getTitle
	lift $ do
	    drawButtons mode
	    drawUIOptionButtons mode
	    drawMsgLine
	    refresh
	where
	drawMainState' (PlayState { psCurrentState=st, psLastAlerts=alerts, wrenchSelected=wsel }) = do
	    canUndo <- null <$> gets psGameStateMoveStack
	    canRedo <- null <$> gets psUndoneStack
	    lift $ do
		let selTools = [ idx |
			(idx, PlacedPiece pos p) <- enumVec $ placedPieces st
			, or [wsel && isWrench p, not wsel && isHook p] ]
		drawMainGameState selTools False alerts st
		registerUndoButtons canUndo canRedo
	drawMainState' (ReplayState { rsCurrentState=st } ) = do
	    canUndo <- null <$> gets rsGameStateMoveStack
	    canRedo <- null <$> gets rsMoveStack
	    lift $ do
		drawMainGameState [] False [] st
		registerUndoButtons canUndo canRedo
		renderToMain $ drawCursorAt Nothing
	drawMainState' (EditState { esGameStateStack=(st:sts), esUndoneStack=undostack,
		selectedPiece=selPiece, selectedPos=selPos }) = lift $ do
	    drawMainGameState (maybeToList selPiece) True [] st
	    renderToMain $ drawCursorAt $ if isNothing selPiece then Just selPos else Nothing
	    registerUndoButtons (null sts) (null undostack)
	    when (isJust selPiece) $ mapM_ registerButtonGroup
		[ singleButton (periphery 2 <+> 3<*>hw<+>hu) CmdDelete 0
		, singleButton (periphery 2 <+> 3<*>hw) CmdMerge 4
		]
	    drawPaintButtons
	drawMainState' (MetaState saddr undecls cOnly auth names _ _ rnamestvar _ _ mretired path mlock offset) = do
	    let ourName = authUser <$> auth
	    let selName = listToMaybe names
	    let home = isJust ourName && ourName == selName
	    lift $ renderToMain $ (erase >> drawCursorAt Nothing)
	    lift $ maybe (drawEmptyMiniLock miniLockPos)
		(\lock -> do
		    drawMiniLock lock miniLockPos
		    registerSelectable miniLockPos 3 SelOurLock
		    registerButtonGroup $ singleButton (miniLockPos <+> 2<*>neg hv <+> hu) CmdPrevLock 4
		    registerButtonGroup $ singleButton (miniLockPos <+> 2<*>neg hv <+> hu <+> neg hw) CmdNextLock 4
		    )
		(fst<$>mlock)
	    lift $ do
		smallFont <- gets dispFontSmall
		renderToMain $ withFont smallFont $ renderStrColAtLeft messageCol
			(saddrStr saddr ++ if cOnly then " (cache only)" else "")
		    $ serverPos <+> hu
		renderToMain $ renderStrColAtLeft messageCol path $ lockLinePos <+> hu
		when (offset>0) $
		    registerButtonGroup $ singleButton (nextPagePos<+>neg hu) CmdPrevPage 4

	    when (length names > 1) $ lift $ registerButtonGroup $ singleButton
		(codenamePos <+> 2<*>neg hu <+> hw) CmdBackCodename 0

	    runMaybeT $ do
		name <- MaybeT (return selName)
		FetchedRecord fresh err muirc <- lift $ getUInfoFetched 300 name
		lift $ do
		    lift $ do
			unless fresh $ do
			    smallFont <- gets dispFontSmall
			    renderToMain $ withFont smallFont $ renderStrColAtLeft (opaquify $ dim errorCol) "(stale)" $ serverWaitPos
			maybe (return ()) (setMsgLineNoRefresh errorCol) err
			when (fresh && (isNothing ourName || home || isNothing muirc)) $
			    registerButtonGroup $ singleButton (codenamePos <+> 2<*>hu)
				(if (isNothing muirc && isNothing ourName) || home then CmdRegister else CmdAuth) 2
		    (if isJust muirc then drawName else drawNullName) name codenamePos
		    lift $ registerSelectable codenamePos 0 SelSelectedCodeName
		    drawRelScore name (codenamePos<+>hu)
		    for_ muirc $ \(RCUserInfo (_,uinfo)) -> case mretired of 
			    Just retired -> do
				fillArea locksPos
				    (map (locksPos<+>) $ zero:[rotate n $ 4<*>hu<->4<*>hw | n <- [0,2,3,5]])
				    [ \pos -> (lift $ registerSelectable pos 1 (SelOldLock ls)) >> drawOldLock ls pos
				    | ls <- retired ]
				lift $ registerButtonGroup $ singleButton retiredPos CmdShowRetired pubWheelAngle
				lift $ registerButtonGroup $ singleButton (retiredPos <+> hw) (CmdPlayLockSpec Nothing) 4
			    Nothing -> do
				sequence_ [ drawLockInfo (ActiveLock (codename uinfo) i) mlockinfo |
				    (i,mlockinfo) <- assocs $ userLocks uinfo ]
				let mmiscpos = (serverPos <+> 2<*>neg hv)
				when (isJust $ msum $ elems $ userLocks uinfo) $ lift $ do
				    registerButtonGroup
					([ Button mmiscpos (CmdSolve Nothing), Button (mmiscpos<+>hu) (CmdViewSolution Nothing)], (2,2))
				let tested = maybe False (isJust.snd) mlock
				when (isJust mlock && home) $ lift $ registerButtonGroup $
					singleButton (miniLockPos <+> 2<*>hv <+> neg hu) (CmdPlaceLock Nothing) $ if tested then 2 else 0
				lift $ registerButtonGroup $ singleButton retiredPos CmdShowRetired pubWheelAngle

	    when home $ do
		unless (null undecls) $ do
		    lift.renderToMain $ renderStrColAtLeft messageCol "Undeclared:" (undeclsPos<+>2<*>hv)
		    lift $ registerButtonGroup $ singleButton (undeclsPos<+>2<*>hv<+>neg hu) (CmdDeclare Nothing) 2
		    fillArea (undeclsPos<+>hv)
			(map (undeclsPos<+>) $ hexDisc 1 ++ [hu<+>neg hw, neg hu<+>hv])
			[ \pos -> (lift $ registerSelectable pos 0 (SelUndeclared undecl)) >> drawActiveLock al pos
			| undecl@(Undeclared _ _ al) <- undecls ]
		rnames <- liftIO $ atomically $ readTVar rnamestvar
		unless (null rnames) $
		    fillArea randomNamesPos
			(map (randomNamesPos<+>) $ hexDisc 2)
			[ \pos -> (lift $ registerSelectable pos 0 (SelRandom name)) >> drawName name pos
			| name <- rnames ]

	    when (ourName /= selName) $ void $ runMaybeT $ do
		when (isJust ourName) $
		    lift.lift $ registerButtonGroup $ singleButton (codenamePos <+> neg hu <+> hw) CmdHome 4
		sel <- MaybeT $ return selName
		us <- MaybeT $ return ourName
		ourUInfo <- mgetUInfo us
		selUInfo <- mgetUInfo sel
		let accesses = map (uncurry getAccessInfo) [(ourUInfo,sel),(selUInfo,us)]
		lift $ do 
		    fillArea (codenamePos<+>3<*>hv<+>hw) (map ((codenamePos<+>3<*>hv)<+>) [zero,hw,neg hv])
			[ \pos -> (lift $ registerSelectable pos 0 SelOurAL) >>
			    drawNameWithCharAndCol us white (lockIndexChar i) col pos
			| i <- [0..2]
			, let (pub,access) = accesses !! 0 !! i
			, let col
				| pub = obscure pubColour 
				| access = obscure $ scoreColour $ -3 
				| otherwise = dim $ scoreColour 3 ]
		    fillArea (codenamePos<+>2<*>neg hw) (map ((codenamePos<+>3<*>neg hw)<+>) [zero,hw,neg hv])
			[ \pos -> (lift $ registerSelectable pos 0 (SelLock $ ActiveLock sel i)) >>
			    drawNameWithCharAndCol sel white (lockIndexChar i) col pos
			| i <- [0..2]
			, let (pub,access) = accesses !! 1 !! i
			, let col
				| pub = obscure pubColour 
				| access = obscure $ scoreColour $ 3 
				| otherwise = dim $ scoreColour $ -3 ]
	drawMainState' _ = return ()

    drawMessage = say
    drawError = sayError

    drawAlerts alerts = return ()

    getChRaw = do
	events <- liftIO getEvents
	case listToMaybe $ [ key | KeyDown key <- events ] of
	    Nothing -> getChRaw
	    Just (Keysym _ _ ch) -> return $ Just ch

    setUIBinding mode cmd ch =
	modify $ \s -> s { uiKeyBindings =
		Map.insertWith (\[bdg] -> \bdgs -> if bdg `elem` bdgs then delete bdg bdgs else bdgs ++ [bdg])
		    mode [(ch,cmd)] $ uiKeyBindings s }

    initUI = liftM isJust (runMaybeT $ do
	catchIOErrorMT $ SDL.init [InitVideo]
	catchIOErrorMT TTF.init
	lift $ do
	    readUIConfigFile
	    initVideo 0 0
	    liftIO $ setCaption "intricacy" "intricacy"
	    w <- gets scrWidth
	    h <- gets scrHeight
	    liftIO $ warpMouse (fromIntegral $ w`div`2) (fromIntegral $ h`div`2)
	    renderToMain $ erase
	    liftIO $ enableUnicode True
	    liftIO $ enableKeyRepeat 250 30
	    readBindings
	)
	where
	    catchIOErrorMT m = MaybeT $ liftIO $ catchIO (m >> return (Just ())) (\_ -> return Nothing)

    endUI = do
	writeUIConfigFile
	writeBindings
	liftIO $ quit

    unblockInput = return $ pushEvent VideoExpose
    suspend = return ()
    redraw = return ()

    getDrawImpatience = do
	curState <- get
	let pos = serverWaitPos
	return $ \ticks -> void $ flip runStateT curState $ do 
	    when (ticks>2) $ renderToMain $ do
		mapM (drawAtRel (filledHexGlyph $ bright black)) [ pos <+> i<*>hu | i <- [0..3] ]
		withFont (dispFontSmall curState) $
		    renderStrColAtLeft errorCol ("waiting..."++replicate (ticks`mod`3) '.') $ pos
	    refresh

    warpPointer pos = do
	(scrCentre, size) <- getGeom
	centre <- gets dispCentre
	let SVec x y = hexVec2SVec size (pos<->centre) <+> scrCentre
	liftIO $ warpMouse (fromIntegral x) (fromIntegral y)
	lbp <- gets leftButtonDown
	rbp <- gets rightButtonDown
	let [lbp',rbp'] = fmap (fmap (\_ -> (pos<->centre))) [lbp,rbp]
	modify $ \s -> s {leftButtonDown = lbp', rightButtonDown = rbp'}

    getInput mode = do
	events <- liftIO $ getEvents
	oldUIState <- get
	cmds <- concat <$> mapM processEvent events
	newUIState <- get
	return $ cmds ++ if uistatesMayVisiblyDiffer oldUIState newUIState then [CmdRefresh] else []
	where
	    uistatesMayVisiblyDiffer uis1 uis2 =
		uis1 { mousePos = (zero,False), lastFrameTicks=0 }
		/= uis2 {mousePos = (zero,False), lastFrameTicks=0 }
	    processEvent (KeyDown (Keysym _ _ ch)) = case mode of
		IMTextInput -> return [CmdInputChar ch]
		_ -> do
		    setting <- gets settingBinding
		    if isJust setting && ch /= '\0'
		    then do
			modify $ \s -> s {settingBinding = Nothing}
			when (ch /= '\ESC') $ setUIBinding mode (fromJust setting) ch
			return []
		    else do
			uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
			let mCmd = lookup ch $ uibdgs ++ bindings mode
			sequence_
			    [ modify $ \s -> s { paintTileIndex = pti }
				| (pti,pt) <- zip [0..] paintTiles
				, (isNothing pt && mCmd == Just CmdDelete) ||
				    (isJust $ do
					pt' <- pt
					CmdTile t <- mCmd
					guard $ ((==)`on`tileType) t pt') ]
			return $ maybeToList mCmd
	    processEvent (MouseMotion {}) = do
		(oldMPos,_) <- gets mousePos
		(pos@(mPos,_),(sx,sy,sz)) <- getMousePos
		updateMousePos mode pos
		lbp <- gets leftButtonDown
		rbp <- gets rightButtonDown
		centre <- gets dispCentre
		let drag :: Maybe HexVec -> Maybe Command
		    drag bp = do
			fromPos@(HexVec x y z) <- bp
			-- check we've dragged at least a full hex's distance:
			guard $ not.all (\(a,b) -> abs ((fromIntegral a) - b) < 1.0) $ [(x,sx),(y,sy),(z,sz)]
			let dir = hexVec2HexDirOrZero $ mPos <-> fromPos
			guard $ dir /= zero
			return $ CmdDrag (fromPos<+>centre) dir
		case mode of
		    IMEdit -> case drag rbp of
			Just cmd -> return [cmd]
			Nothing -> if mPos /= oldMPos
			    then do
				pti <- getEffPaintTileIndex
				return $ [ CmdMoveTo $ mPos <+> centre ] ++
				    (if isJust lbp then [ CmdPaintFromTo (paintTiles!!pti) (oldMPos<+>centre) (mPos<+>centre) ] else [])
			    else return []
		    IMPlay -> return $ maybeToList $ msum $ map drag [lbp, rbp]
		    _ -> return []
		where
		    mouseFromTo from to = do
			let dir = hexVec2HexDirOrZero $ to <-> from
			if dir /= zero
			    then (CmdDir WHSSelected dir:) <$> mouseFromTo (from <+> dir) to
			    else return []
	    processEvent (MouseButtonDown _ _ ButtonLeft) = do
		pos@(mPos,central) <- gets mousePos
		modify $ \s -> s { leftButtonDown = Just mPos }
		rb <- isJust <$> gets rightButtonDown
		mcmd <- cmdAtMousePos pos mode (Just False)
		let hotspotAction = listToMaybe
			$ map (\cmd -> return [cmd]) (maybeToList mcmd)
			++ [ (modify $ \s -> s {paintTileIndex = i}) >> return []
			    | i <- take (length paintTiles) [0..]
			    , mPos == paintButtonStart <+> i<*>hv ]
			++ [ toggleUIOption uiOB1 >> updateHoverStr >> return []
			    | mPos == uiOptPos uiOB1 ]
			++ [ toggleUIOption uiOB2 >> updateHoverStr >> return []
			    | mPos == uiOptPos uiOB2 ]
			++ [ toggleUIOption uiOB3 >> updateHoverStr >> return []
			    | mPos == uiOptPos uiOB3 ]

		if rb
		then return [ CmdWait ]
		else flip fromMaybe hotspotAction $ case mode of
		    IMEdit -> do
			pti <- getEffPaintTileIndex
			return $ [ drawCmd (paintTiles!!pti) False ]
		    IMPlay -> do
			centre <- gets dispCentre
			return $ [ CmdManipulateToolAt $ mPos <+> centre ]
		    _ -> return []
	    processEvent (MouseButtonUp _ _ ButtonLeft) = do
		modify $ \s -> s { leftButtonDown = Nothing }
		return []
	    processEvent (MouseButtonDown _ _ ButtonRight) = do
		pos@(mPos,_) <- gets mousePos
		modify $ \s -> s { rightButtonDown = Just mPos }
		(fromMaybe [] <$>) $ runMaybeT $ msum
		    [ do
			cmd <- MaybeT $ cmdAtMousePos pos mode Nothing
			modify $ \s -> s { settingBinding = Just cmd }
			return []
		    , do
			cmd <- MaybeT $ cmdAtMousePos pos mode (Just True)
			return [cmd]
		    , case mode of
			IMPlay -> do
			    centre <- gets dispCentre
			    return $ [ CmdManipulateToolAt $ mPos <+> centre ]
			_ -> return [ CmdSelect ] ]
	    processEvent (MouseButtonUp _ _ ButtonRight) = do
		modify $ \s -> s { rightButtonDown = Nothing }
		return [ CmdUnselect ]
	    processEvent (MouseButtonDown _ _ ButtonWheelUp) = doWheel 1
	    processEvent (MouseButtonDown _ _ ButtonWheelDown) = doWheel $ -1
	    processEvent (MouseButtonDown _ _ ButtonMiddle) = do
		(mPos,_) <- gets mousePos
		modify $ \s -> s { middleButtonDown = Just mPos }
		rb <- isJust <$> gets rightButtonDown
		return $ if rb then [ CmdDelete ] else []
	    processEvent (MouseButtonUp _ _ ButtonMiddle) = do
		modify $ \s -> s { middleButtonDown = Nothing }
		return []
	    processEvent (VideoResize w h) = do
		initVideo w h
		return [ CmdRedraw ]
	    processEvent VideoExpose = return [ CmdRedraw ]
	    processEvent Quit = return [ CmdForceQuit ]
		
	    processEvent _ = return []

	    doWheel dw = do
		rb <- isJust <$> gets rightButtonDown
		mb <- isJust <$> gets middleButtonDown
		if rb || mode == IMPlay && not mb
		    then return [ CmdRotate dw ]
		    else if mb 
			then return [ if dw == 1 then CmdUndo else CmdRedo ]
			else do
			modify $ \s -> s { paintTileIndex = (paintTileIndex s + dw) `mod` (length paintTiles) }
			return []
		

	    drawCmd mt True = CmdPaint mt
	    drawCmd (Just t) False = CmdTile t
	    drawCmd Nothing _ = CmdDelete

	    getMousePos :: UIM ((HexVec,Bool),(Double,Double,Double))
	    getMousePos = do
		(scrCentre, size) <- getGeom
		(x,y,_) <- lift getMouseState
		let sv = (SVec (fromIntegral x) (fromIntegral y)) <+> neg scrCentre
		let mPos@(HexVec x y z) = sVec2HexVec size sv
		let (sx,sy,sz) = sVec2dHV size sv
		let isCentral = all (\(a,b) -> abs ((fromIntegral a) - b) < 0.5) $
			[(x,sx),(y,sy),(z,sz)]
		return ((mPos,isCentral),(sx,sy,sz))
	    updateMousePos mode newPos = do
		oldPos <- gets mousePos
		when (newPos /= oldPos) $ do
		    modify $ \ds -> ds { mousePos = newPos }
		    updateHoverStr 

	    updateHoverStr = do
		p@(mPos,isCentral) <- gets mousePos
		hstr <- runMaybeT $ msum
		    [ MaybeT ( cmdAtMousePos p mode Nothing ) >>= lift . describeCommandAndKeys
		    , guard (mPos == uiOptPos uiOB1) >> describeUIOptionButton uiOB1
		    , guard (mPos == uiOptPos uiOB2) >> describeUIOptionButton uiOB2
		    , guard (mPos == uiOptPos uiOB3) >> describeUIOptionButton uiOB3 ]
		modify $ \ds -> ds { hoverStr = hstr }
	    describeCommandAndKeys :: Command -> UIM String
	    describeCommandAndKeys cmd = do
		uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
		return $ describeCommand cmd ++ " ["
		    ++ concat (intersperse ","
			(map showKeyFriendly $ findBindings (uibdgs ++ bindings mode) cmd))
		    ++ "]"

    showHelp mode = do
	bdgs <- nub <$> getBindings mode
	smallFont <- gets dispFontSmall
	renderToMain $ do 
	    erase
	    let bdgWidth = (screenWidthHexes-6) `div` 3
		showKeys chs = intercalate "/" (map showKeyFriendly chs)
		maxkeyslen = maximum $ map (length.showKeys.map fst) $ groupBy ((==) `on` snd) bdgs
		mouseHelpStrs = ["Mouse commands:", "Right-click on a button to set a keybinding;"] ++ case mode of
		    IMPlay -> ["Click on tool to select, drag to move;",
			"Click by tool to move;", "Scroll wheel to rotate hook;",
			"Scroll wheel while held down to undo/redo."]
		    IMEdit -> ["Left-click to draw selected; scroll to change selection;",
			"Right-click on piece to select, drag to move;",
			"While holding right-click: left-click to advance time, middle-click to delete;",
			"Scroll wheel to rotate selected piece; scroll wheel while held down to undo/redo."]
		    IMReplay -> ["Scroll wheel while held down to undo/redo."]
		    IMMeta -> ["Left-clicking on something does most obvious thing;",
			"Right-clicking does second-most obvious thing."]

	    renderStrColAt messageCol "Keybindings:" $ (screenHeightHexes`div`4)<*>(hv<+>neg hw)
	    sequence_ [ with $ renderStrColAtLeft messageCol
			( keysStr ++ ": " ++ desc )
			$ (x*bdgWidth-(screenWidthHexes-6)`div`2)<*>hu <+> neg hv <+>
			  (screenHeightHexes`div`4 - y`div`2)<*>(hv<+>neg hw) <+>
			  (y`mod`2)<*>hw
		| ((keysStr,with,desc),(x,y)) <- zip [(keysStr,with,desc)
			| group <- groupBy ((==) `on` snd) $ sortBy (compare `on` snd) bdgs
			, let cmd = snd $ head group
			, let desc = describeCommand cmd
			, not $ null desc
			, let chs = map fst group
			, let keysStr = showKeys chs
			, let with = if True -- 3*(bdgWidth-1) < length desc + length keysStr + 1
				then withFont smallFont
				else id
			]
		    (map (`divMod` (screenHeightHexes-9)) [0..])
		, (x+1)*bdgWidth < screenWidthHexes]
	    sequence_ [ renderStrColAt messageCol str
			$ (3-screenHeightHexes`div`4-y`div`2)<*>(hv<+>neg hw)
			  <+> hv
			  <+> (y`mod`2)<*>hw
		| (str,y) <- zip mouseHelpStrs [0..] ]
	refresh

getEvents = do
    e <- waitEvent
    es <- pollEvents
    return $ e:es

pollEvents = do
    e <- pollEvent
    case e of
	NoEvent -> return []
	_ -> do
	    es <- pollEvents
	    return $ e:es


fillArea :: HexVec -> [HexVec] -> [HexVec -> StateT MainState UIM ()] -> StateT MainState UIM () 
fillArea centre area draws = do
    selDraws <- do
	    offset <- gets listOffset
	    let na = length area
		nd = length draws
	    when (nd > (na*(offset+1))) $ lift $
		registerButtonGroup $ singleButton nextPagePos CmdNextPage 4
	    return $ drop (max 0 $ min (nd - na) (na*offset)) $ draws
    sequence_ $ map (uncurry ($)) $
	zip selDraws $ sortBy
		(compare `on` (hexLen . (<->centre)))
		area

drawOldLock ls pos = void.runMaybeT $ msum [ do
	lock <- mgetLock ls
	lift.lift $ drawMiniLock lock pos
    , lift.lift.renderToMain $
	    renderStrColAt messageCol (show ls) pos
    ]
    

drawName name pos = nameCol name >>= drawNameCol name pos
drawNullName name pos = drawNameCol name pos $ invisible white
drawNameCol name pos col = do
    lift.renderToMain $ do 
	drawAtRel (playerGlyph col) pos
	renderStrColAt messageCol name pos
drawRelScore name pos = do
    col <- nameCol name
    relScore <- getRelScore name
    flip (maybe (return ())) relScore $ \score ->
	lift.renderToMain $ renderStrColAt col
	    ((if score > 0 then "+" else "") ++ show score) pos

drawNote note pos = case noteBehind note of
    Just al -> drawActiveLock al pos
    Nothing -> drawPublicNote (noteAuthor note) pos
drawActiveLock (ActiveLock name i) =
    drawNameWithChar name white (lockIndexChar i)
drawPublicNote name =
    drawNameWithChar name pubColour 'P'
drawNameWithChar name charcol char pos = do
    col <- nameCol name
    drawNameWithCharAndCol name charcol char col pos
drawNameWithCharAndCol name charcol char col pos = do
    size <- fromIntegral.snd <$> lift getGeom
    let up = SVec 0 $ - (ysize size - size`div`2)
    let down = SVec 0 $ ysize size
    smallFont <- lift $ gets dispFontSmall
    lift.renderToMain $ do
	drawAtRel (playerGlyph col) pos
	displaceRender up $ 
	    renderStrColAt messageCol name pos
	displaceRender down $ withFont smallFont $
	    renderStrColAt charcol [char] pos
pubWheelAngle = 5
pubColour = colourWheel pubWheelAngle -- ==purple
nameCol name = do
    ourName <- (authUser <$>) <$> gets curAuth
    relScore <- getRelScore name
    return $ dim $ case relScore of
	    Nothing -> Pixel $ if ourName == Just name then 0xc0c0c000 else 0x80808000
	    Just score -> scoreColour score
scoreColour :: Int -> Pixel
scoreColour score = Pixel $ case score of
	0 -> 0x80800000
	1 -> 0x70a00000 
	2 -> 0x40c00000
	3 -> 0x00ff0000
	(-1) -> 0xa0700000 
	(-2) -> 0xc0400000
	(-3) -> 0xff000000

drawLockInfo :: ActiveLock -> Maybe LockInfo -> StateT MainState UIM ()
drawLockInfo al@(ActiveLock name i) Nothing = do
    let centre = hw<+>neg hv <+> 7*(i-1)<*>hu
    lift $ drawEmptyMiniLock centre
    drawNameWithCharAndCol name white (lockIndexChar i) (invisible white) centre
    lift $ registerSelectable centre 3 $ SelLockUnset al
drawLockInfo al@(ActiveLock name i) (Just lockinfo) = do
    let centre = locksPos <+> 7*(i-1)<*>hu
    let accessedByPos = centre <+> 3<*>(hv <+> neg hw)
    let accessedPos = centre <+> 2<*>(hw <+> neg hv)
    let notesPos = centre <+> 3<*>(hw <+> neg hv)
    ourName <- (authUser <$>) <$> gets curAuth
    runMaybeT $ msum [
	do
	    lock <- mgetLock $ lockSpec lockinfo
	    lift.lift $ do
		drawMiniLock lock centre
		registerSelectable centre 3 $ SelLock al
	, lift $ do
		drawActiveLock al centre
		lift $ registerSelectable centre 3 $ SelLock al
	]

    lift.renderToMain $ renderStrColAt dimWhiteCol "Accessed by:" $ accessedByPos <+> hv
    if public lockinfo
    then lift.renderToMain $ renderStrColAt pubColour "Everyone!" accessedByPos
    else if null $ accessedBy lockinfo
	then lift.renderToMain $ renderStrColAt messageCol "No-one" accessedByPos
	else fillArea accessedByPos
		[ accessedByPos <+> d | j <- [0..2], i <- [-2..3]
		    , i-j > -4, i-j < 3
		    , let d = j<*>hw <+> i<*>hu ]
		$ [ \pos -> (lift $ registerSelectable pos 0 (SelSolution note)) >> drawNote note pos
		    | note <- lockSolutions lockinfo ] ++
		[ \pos -> (lift $ registerSelectable pos 0 (SelAccessed name)) >> drawName name pos
		    | name <- accessedBy lockinfo \\ map noteAuthor (lockSolutions lockinfo) ]
    
    undecls <- gets undeclareds
    if isJust $ guard . (|| public lockinfo) . (`elem` map noteAuthor (lockSolutions lockinfo)) =<< ourName
    then lift.renderToMain $ 
	(if public lockinfo
	    then renderStrColAt pubColour "Accessed!"
	    else renderStrColAt green "Solved!")
	accessedPos
    else if any (\(Undeclared _ ls _) -> ls == lockSpec lockinfo) undecls
    then lift.renderToMain $ renderStrColAt yellow "Undeclared" accessedPos
    else do
	read <- take 3 <$> getNotesReadOn lockinfo
	unless (ourName == Just name) $ do
	    lift.renderToMain $ renderStrColAt (if length read == 3 then accessedCol else dimWhiteCol)
		"Read:" $ accessedPos <+> (-3)<*>hu
	    fillArea (accessedPos<+>neg hu) [ accessedPos <+> i<*>hu | i <- [-1..1] ]
		$ take 3 $ [ \pos -> (lift $ registerSelectable pos 0 (SelReadNote note)) >> drawName (noteAuthor note) pos
		    | note <- read ] ++ (repeat $ lift . renderToMain . drawAtRel (hollowGlyph $ dim green))

    lift.renderToMain $ renderStrColAt dimWhiteCol "Holds notes:" $ notesPos <+> hv
    if null $ notesSecured lockinfo
	then lift.renderToMain $ renderStrColAt messageCol "None" notesPos
	else fillArea notesPos
		[ notesPos <+> d | j <- [0..2], i <- [-2..3]
		    , i-j > -4, i-j < 3
		    , let d = j<*>hw <+> i<*>hu ]
		[ \pos -> (lift $ registerSelectable pos 0 (SelSecured note)) >> drawActiveLock (noteOn note) pos
		    | note <- notesSecured lockinfo ]