packages feed

gruff-0.3: src/gruff.hs

module Main (main) where

import Prelude hiding (catch, log)
import Control.Concurrent (forkIO)
import Control.Monad (forM_)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef, writeIORef)
import Data.Maybe (mapMaybe)
import Graphics.UI.Gtk hiding (get, Region, Size, Window, Viewport)
import qualified Graphics.UI.Gtk as GTK
import Graphics.UI.Gtk.OpenGL (initGL)
import System.Directory (getAppUserDataDirectory, createDirectoryIfMissing)
import System.FilePath ((</>))

import Fractal.RUFF.Types.Complex (Complex((:+)), realPart, imagPart)

import Paths_gruff (getDataFileName)
import Number (R)
import Browser (Browser(..), browserNew, browserRenders)
import Progress (progressNew)
import Interact (mouseCallbacks)
import View
  ( Image(..), Location(..), Viewport(..), Window(..)
  , Colours(..), Colour(..), defImage, defLocation
  )
import GLUTGtk (glut, Size(Size), postRedisplay)
import Logger (logger, LogLevel(Debug))
import qualified Logger as Log
import Utils (safeRead, catchIO)

exit :: (LogLevel -> String -> IO ()) -> FilePath -> Gruff -> IO ()
exit lg stateFile g = do
  lg Debug "exitCallback"
  aDoSave stateFile g
  mainQuit

data GruffGUI = GruffGUI
  -- buttons
  { bHome
  , bLoad
  , bSave
  , bStop
  , bClear                  :: Button
  -- entries
  , eRealM, eRealE
  , eImagM, eImagE
  , eSizeM, eSizeE
  , eRota
  , eWidth
  , eHeight
  , eSamples                :: Entry
  -- colour pickers
  , cInterior
  , cBorder
  , cExterior               :: ColorButton
  -- windows
  , wMain
  , wImage                  :: GTK.Window
  }

main :: IO ()
main = do
  -- contexts
  _ <- initGUI
  _ <- initGL
  gl' <- glut minSize
  -- directories
  appDir <- getAppUserDataDirectory "gruff"
  let cacheDir' = appDir </> "cache"
      logDir    = appDir </> "log"
      stateFile = appDir </> "state.gruff"
  createDirectoryIfMissing False appDir
  createDirectoryIfMissing False logDir
  lg <- logger logDir
  icon <- pixbufNewFromFile =<< getDataFileName "icon.png"
  browser <- browserNew gl' icon lg cacheDir'
  let iw = browserWindow browser
  sg <- sizeGroupNew SizeGroupHorizontal

  let spacing = 2

      entryNewWithMnemonic m = entryNewWithMnemonic' m 30

      entryNewWithMnemonic' m wc = do
        e <- entryNew
        entrySetWidthChars e wc
        l <- labelNewWithMnemonic m
        labelSetMnemonicWidget l e
        sizeGroupAddWidget sg l
        h <- hBoxNew False spacing
        boxPackStart h l PackNatural 0
        boxPackStartDefaults h e
        return (e, h)

      entryNewExponent = do
        e <- entryNew
        entrySetWidthChars e 4
        l <- labelNew (Just "e")
        h <- hBoxNew False spacing
        boxPackStart h l PackNatural 0
        boxPackStart h e PackNatural 0
        return (e, h)

      frameNewWithContents box t r ws = do
        f <- frameNew
        frameSetLabel f t
        frameSetLabelAlign f (if r then 1 else 0) 0.5
        v <- box False spacing
        forM_ ws $ boxPackStartDefaults v
        set f [ containerChild := v ]
        return f

  b01@bHome'  <- buttonNewWithLabel "Home"
  b02@bLoad'  <- buttonNewWithLabel "Load"
  b03@bSave'  <- buttonNewWithLabel "Save"
  b04@bStop'  <- buttonNewWithLabel "Stop"
  b05@bClear' <- buttonNewWithLabel "Clear"

  (eRealM', fc1m) <- entryNewWithMnemonic "_Real"
  (eRealE', fc1e) <- entryNewExponent
  (eImagM', fc2m) <- entryNewWithMnemonic "_Imag"
  (eImagE', fc2e) <- entryNewExponent
  (eSizeM', fc3m) <- entryNewWithMnemonic "Si_ze"
  (eSizeE', fc3e) <- entryNewExponent
  (eRota', fvR)    <- entryNewWithMnemonic' "R_otation" 15
  (eWidth', fvW)   <- entryNewWithMnemonic' "_Width"     5
  (eHeight', fvH)  <- entryNewWithMnemonic' "_Height"    5
  (eSamples', fvS) <- entryNewWithMnemonic' "_Samples"   3

  cInterior' <- colorButtonNewWithColor red
  cBorder'   <- colorButtonNewWithColor black
  cExterior' <- colorButtonNewWithColor white

  labelColourR <- newIORef (fromColor blue)
  cLabels'   <- colorButtonNewWithColor blue
  _ <- cLabels' `onColorSet` (colorButtonGetColor cLabels' >>= writeIORef labelColourR . fromColor)

  fb <- frameNewWithContents  vBoxNew "Actions" False [b01, b02, b03, b04, b05]
  fh <- frameNewWithContents  hBoxNew "Colours" True [cInterior', cBorder', cExterior', cLabels']
  fv <- frameNewWithContents  hBoxNew "Viewport" True [fvR, fvW, fvH, fvS]

  let packMantissaExponent m e = do
        h <- hBoxNew False spacing
        boxPackStartDefaults h m
        boxPackStart h e PackNatural 0
        return h
  fc1 <- packMantissaExponent fc1m fc1e
  fc2 <- packMantissaExponent fc2m fc2e
  fc3 <- packMantissaExponent fc3m fc3e
  fc <- frameNewWithContents vBoxNew "Coordinates" True [fc1, fc2, fc3]
  v <- vBoxNew False spacing
  mapM_ (\w -> boxPackStart v w PackNatural 0) [fc, fv, {- fa, -} fh]
  h <- hBoxNew False spacing
  boxPackStart h fb PackNatural 0
  boxPackStartDefaults h v

  ww <- windowNew
  set ww [ windowIcon := Just icon, windowTitle := "gruff control" ]

  containerAdd ww h
  mg <- aDoLoad stateFile
  gR <- newIORef $ case mg of
    Nothing -> initialGruff
    Just g -> g

  let g0 = GruffGUI
            { bHome                 = bHome'
            , bLoad                 = bLoad'
            , bSave                 = bSave'
            , bStop                 = bStop'
            , bClear                = bClear'
            , eRealM                = eRealM'
            , eRealE                = eRealE'
            , eImagM                = eImagM'
            , eImagE                = eImagE'
            , eSizeM                = eSizeM'
            , eSizeE                = eSizeE'
            , eRota                 = eRota'
            , eWidth                = eWidth'
            , eHeight               = eHeight'
            , eSamples              = eSamples'
            , cInterior             = cInterior'
            , cBorder               = cBorder'
            , cExterior             = cExterior'
            , wMain                 = ww
            , wImage                = iw
            }
      but b a = do
        _ <- b g0 `onClicked` wrapA g0 gR a
        return ()
      butI b a = do
        _ <- b g0 `onClicked` (wrapA g0 gR a >> upI)
        return ()

      butO b a = do
        _ <- b g0 `onClicked` a
        return ()

      entI e a = do
        _ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a >> upI)
        return ()

      colI cbi cbb cbe a = forM_ [cbi, cbb, cbe] $ \c -> do
        _ <- c g0 `onColorSet` (do
          ci <- colorButtonGetColor (cbi g0)
          cb <- colorButtonGetColor (cbb g0)
          ce <- colorButtonGetColor (cbe g0)
          _ <- a ci cb ce gR
          upI)
        return ()

      entME m e a = do
        let a' = do
                  ms <- entryGetText (m g0)
                  es <- entryGetText (e g0)
                  let s = ms ++ if null es then "" else "e" ++ es
                  wrapE g0 gR a s
                  upI
        _ <- m g0 `onEntryActivate` a'
        _ <- e g0 `onEntryActivate` a'
        return ()

      upI :: IO ()
      upI = do
        g <- readIORef gR
        browserRender browser g (return ()) (return ())
        postRedisplay gl'

      aUpdate :: Maybe View.Image -> IO ()
      aUpdate Nothing = return ()
      aUpdate (Just i) = do
        writeIORef gR i
        uReal g0 i
        uImag g0 i
        uSize g0 i
        uRota g0 i
        upI

      aReshape :: Int -> Int -> IO ()
      aReshape w' h' = do
        atomicModifyIORef gR $ \g ->
          ( g { imageWindow = (imageWindow g){ width = w', height = h' }
              , imageViewport = (imageViewport g){ aspect = fromIntegral w' / fromIntegral h' }
              }
          , () )
        g <- readIORef gR
        uEverything g0 g
        upI

  butI bHome aHome
  butI bLoad aLoad
  but  bSave aSave
  butO bStop (browserAbort browser)
  butI bClear aClear

  entME eRealM eRealE aReal
  entME eImagM eImagE aImag
  entME eSizeM eSizeE aSize
  entI eRota aRota
  entI eWidth aWidth
  entI eHeight aHeight
  entI eSamples aSamples
  colI cInterior cBorder cExterior aColours
  let aExit' = (exit (Log.log lg) stateFile =<< readIORef gR)
  browserSetExitCallback browser aExit'

  progress <- progressNew icon
  let mcbs = mouseCallbacks (readIORef labelColourR) progress aUpdate
  browserSetMouseCallback browser mcbs

  browserSetReshapeCallback browser aReshape
  _ <- ww `onDestroy` aExit'
  g <- readIORef gR
  uEverything g0 g
  upI
  widgetShowAll iw
  widgetShowAll ww
  _ <- forkIO $ script browser
  mainGUI

type Gruff = View.Image

initialGruff :: Gruff
initialGruff = defImage

-- button actions

type A = GruffGUI -> Gruff -> IO Gruff

wrapA :: GruffGUI -> IORef Gruff -> A -> IO ()
wrapA g0 gR a = do
  g <- readIORef gR
  g' <- a g0 g
  writeIORef gR $! g'

aHome :: A
aHome g0 g = do
  let g' = g{ imageLocation = defLocation, imageViewport = (imageViewport g){ orient = 0 } }
  uEverything g0 g'
  return g'

aClear :: A
aClear g0 g = do
  let g' = g{ imageLabels = [], imageLines = [] }
  uEverything g0 g'
  return g'

aDoLoad :: FilePath -> IO (Maybe Gruff)
aDoLoad ff = (do
    gr <- safeRead `fmap` readFile ff
    case gr of
      g@(Just _) -> return g
      _ -> putStrLn "file format not supported, sorry" >> return Nothing
  ) `catchIO` const (return Nothing)

aLoad :: A
aLoad g0 g = do
  fc <- fileChooserDialogNew (Just "gruff load") (Just $ wMain g0) FileChooserActionOpen [("Load", ResponseAccept), ("Cancel", ResponseCancel)]
  widgetShow fc
  r <- dialogRun fc
  g' <- case r of
    ResponseAccept -> do
      mf <- fileChooserGetFilename fc
      case mf of
        Nothing -> return g
        Just f -> do
          mg <- aDoLoad f
          case mg of
            Nothing -> return g
            Just g' -> uEverything g0 g' >> return g'
    _ -> return g
  widgetDestroy fc
  return g'

aDoSave :: FilePath -> Gruff -> IO ()
aDoSave f g = writeFile f (show g) `catchIO` const (return ())

aSave :: A
aSave g0 g = do
  fc <- fileChooserDialogNew (Just "gruff save") (Just $ wMain g0) FileChooserActionSave [("Save", ResponseAccept), ("Cancel", ResponseCancel)]
  widgetShow fc
  r <- dialogRun fc
  case r of
    ResponseAccept -> do
      mf <- fileChooserGetFilename fc
      case mf of
        Nothing -> return ()
        Just f -> aDoSave f g
    _ -> return ()
  widgetDestroy fc
  return g

-- entry update

type U = GruffGUI -> Gruff -> IO ()

uEverything, uReal, uImag, uSize, uRota, uColours, uWidth, uHeight, uSamples :: U
uEverything g0 g = forM_ [uReal, uImag, uSize, uRota, uColours, uWidth, uHeight, uSamples] $ \u -> u g0 g
uReal g0 g = uMantissaExponent (eRealM g0) (eRealE g0) (show . (fromRational :: Rational -> R) . realPart . center . imageLocation $ g)
uImag g0 g = uMantissaExponent (eImagM g0) (eImagE g0) (show . (fromRational :: Rational -> R) . imagPart . center . imageLocation $ g)
uSize g0 g = uMantissaExponent (eSizeM g0) (eSizeE g0) (show . radius . imageLocation $ g)
uRota g0 g = entrySetText (eRota g0) (show . orient . imageViewport $ g)
uColours g0 g = do
  colorButtonSetColor (cInterior g0) (fromColour . colourInterior . imageColours $ g)
  colorButtonSetColor (cBorder   g0) (fromColour . colourBoundary . imageColours $ g)
  colorButtonSetColor (cExterior g0) (fromColour . colourExterior . imageColours $ g)
uWidth   g0 g = entrySetText (eWidth   g0) (show . width  . imageWindow $ g)
uHeight  g0 g = entrySetText (eHeight  g0) (show . height . imageWindow $ g)
uSamples g0 g = entrySetText (eSamples g0) (show . supersamples . imageWindow $ g)

uMantissaExponent :: (EntryClass a, EntryClass b) => a -> b -> String -> IO ()
uMantissaExponent m e s = do
  let (ms, me) = break (== 'e') s
  entrySetText m ms
  entrySetText e (drop 1 me)

-- entry actions

type E = Gruff -> String -> Gruff

wrapE :: GruffGUI -> IORef Gruff -> E -> String -> IO ()
wrapE _g0 gR e s = do
  g <- readIORef gR
  let g' = e g s
  writeIORef gR $! g'

aReal, aImag, aSize, aRota, aWidth, aHeight, aSamples :: E
aReal    g s = let _ :+ i = center (imageLocation g) in case safeRead s :: Maybe R of
  Nothing -> g
  Just r -> g{ imageLocation = (imageLocation g){ center = toRational r :+ i } }
aImag    g s = let r :+ _ = center (imageLocation g) in case safeRead s :: Maybe R of
  Nothing -> g
  Just i -> g{ imageLocation = (imageLocation g){ center = r :+ toRational i } }
aSize    g s = case safeRead s :: Maybe Double of
  Nothing -> g
  Just r -> g{ imageLocation = (imageLocation g){ radius = r } }
aRota    g s =  case safeRead s :: Maybe Double of
  Nothing -> g
  Just a -> g{ imageViewport = (imageViewport g){ orient = a } }
aWidth   g s = case safeRead s of
  Nothing -> g
  Just r -> (\g' -> g'{ imageViewport = (imageViewport g'){ aspect = (fromIntegral . width . imageWindow) g' / (fromIntegral . height . imageWindow) g' } }) (g{ imageWindow  = (imageWindow g){ width  = r } })
aHeight  g s = case safeRead s of
  Nothing -> g
  Just r -> (\g' -> g'{ imageViewport = (imageViewport g'){ aspect = (fromIntegral . width . imageWindow) g' / (fromIntegral . height . imageWindow) g' } }) (g{ imageWindow  = (imageWindow g){ height = r } })
aSamples g s = case safeRead s of
  Nothing -> g
  Just r -> g{ imageWindow = (imageWindow g){ supersamples = r } }

aColours :: Color -> Color -> Color -> IORef Gruff -> IO ()
aColours i b e gR = atomicModifyIORef gR $ \g -> (g{ imageColours = Colours{ colourInterior = fromColor i, colourBoundary = fromColor b, colourExterior = fromColor e} }, ())

minSize :: Size
minSize = Size 160 100

red, black, white, blue :: Color
red = Color 65535 0 0
black = Color 0 0 0
white = Color 65535 65535 65535
blue = Color 0 0 65535

fromColor :: Color -> Colour
fromColor (Color r g b) = Colour (fromIntegral r / 65535) (fromIntegral g / 65535) (fromIntegral b / 65535)

fromColour :: Colour -> Color
fromColour (Colour r g b) = Color (round $ r * 65535) (round $ g * 65535) (round $ b * 65535)

script :: Browser -> IO ()
script b = do
  c <- getContents
  case mapMaybe readMay (lines c) of
    [] -> return ()
    images@(_:_) -> browserRenders b images

readMay :: Read a => String -> Maybe a
readMay s = case reads s of
  [(a, "")] -> Just a
  _ -> Nothing