gruff-0.1: gruff.hs
{-# LANGUAGE Rank2Types, ScopedTypeVariables #-}
module Main (main) where
import Prelude hiding (catch, log)
import Control.Monad (forM_, liftM2)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef, writeIORef)
import Data.Maybe (isJust, fromMaybe)
import Data.Ratio (numerator, denominator)
import Graphics.UI.Gtk hiding (get, Region, Size)
import Graphics.UI.Gtk.OpenGL (initGL)
import System.Directory
( getAppUserDataDirectory, createDirectoryIfMissing )
import System.FilePath ((</>))
import Text.PrettyPrint.Leijen.Text (pretty)
import Text.FShow.Raw (DecimalFormat, nanTest, infTest)
import Numeric.QD
import Fractal.RUFF.Mandelbrot.Address (AngledInternalAddress, Angle, parse, angledInternalAddress, externalAngles)
import Fractal.RUFF.Mandelbrot.Ray (externalRay)
import Fractal.RUFF.Types.Complex (Complex((:+)), magnitude)
import GLUTGtk (glut, Size(Size))
import PeriodScan (periodScan, periodNucleus)
import Nucleus (refineNucleus)
import Address (parseAngle, parseAngles, splitAddress, joinAddress, addressPeriod)
import Logger (logger, LogLevel(Debug))
import qualified Logger as Log
import Utils (safeRead, catchIO)
import Paths_gruff (getDataFileName)
import Browser
import StatusDialog
import CacheView (cInitialize)
exit :: (LogLevel -> String -> IO ()) -> FilePath -> Gruff -> IO ()
exit lg stateFile g = do
lg Debug "exitCallback"
aDoSave stateFile g
mainQuit
data GruffGUI = GruffGUI
{ dStatus :: StatusDialog
-- buttons
, bHome
, bLoad
, bSave
, bAddressToCoordinates
, bAddressToIslandChild
, bIslandChildToAddress
, bAddressToAngles
, bIslandToAngles
, bLowerAngleToAddress
, bUpperAngleToAddress
, bPeriodScan :: Button
-- entries
, eAddress
, eIsland
, eChild
, eLowerAngle
, eUpperAngle
, eReal
, eImag
, eSize
, eHueShift
, eHueScale :: Entry
-- windows
, wMain
, wImage :: Window
}
main :: IO ()
main = do
-- contexts
_ <- initGUI
_ <- initGL
gl' <- glut minSize
glC <- 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"
-- widget window
(iw, iUpdate, iInitializeLate) <- iInitialize gl' icon lg cacheDir'
(cw, cUpdate, cInitializeLate) <- cInitialize glC icon cacheDir'
-- widget window
sg <- sizeGroupNew SizeGroupHorizontal
let spacing = 2
entryNewWithMnemonic m = do
e <- entryNew
entrySetWidthChars e 80
l <- labelNewWithMnemonic m
labelSetMnemonicWidget l e
sizeGroupAddWidget sg l
h <- hBoxNew False spacing
boxPackStart h l PackNatural 0
boxPackStartDefaults h e
return (e, h)
frameNewWithContents t r ws = do
f <- frameNew
frameSetLabel f t
frameSetLabelAlign f (if r then 1 else 0) 0.5
v <- vBoxNew False spacing
forM_ ws $ boxPackStartDefaults v
set f [ containerChild := v ]
return f
dStatus' <- statusDialogNew
b01@bHome' <- buttonNewWithLabel "Home"
b02@bLoad' <- buttonNewWithLabel "Load"
b03@bSave' <- buttonNewWithLabel "Save"
b7@bAddressToCoordinates' <- buttonNewWithLabel "Address → Coordinates"
b1@bAddressToIslandChild' <- buttonNewWithLabel "Address → Island + Child"
b2@bIslandChildToAddress' <- buttonNewWithLabel "Island + Child → Address"
b3@bAddressToAngles' <- buttonNewWithLabel "Address → Lower + Upper"
b4@bIslandToAngles' <- buttonNewWithLabel "Island → Lower + Upper"
b5@bLowerAngleToAddress' <- buttonNewWithLabel "Lower → Address"
b6@bUpperAngleToAddress' <- buttonNewWithLabel "Upper → Address"
b8@bPeriodScan' <- buttonNewWithLabel "Period Scan"
(eAddress', fa1) <- entryNewWithMnemonic "_Address"
(eIsland', fa2) <- entryNewWithMnemonic "I_sland"
(eChild', fa3) <- entryNewWithMnemonic "_Child"
(eLowerAngle', fe1) <- entryNewWithMnemonic "_Lower"
(eUpperAngle', fe2) <- entryNewWithMnemonic "_Upper"
(eReal', fc1) <- entryNewWithMnemonic "_Real"
(eImag', fc2) <- entryNewWithMnemonic "_Imag"
(eSize', fc3) <- entryNewWithMnemonic "Si_ze"
(eHueShift', fh1) <- entryNewWithMnemonic "Hue Shift"
(eHueScale', fh2) <- entryNewWithMnemonic "Hue Scale"
b0 <- hBoxNew False spacing
mapM_ (boxPackStartDefaults b0) [b01, b02, b03]
fb <- frameNewWithContents "Actions" False $ toWidget b0 : map toWidget [b7, b1, b2, b3, b4, b5, b6, b8]
fa <- frameNewWithContents "Angled Internal Address" True [fa1, fa2, fa3]
fe <- frameNewWithContents "External Angles" True [fe1, fe2]
fh <- frameNewWithContents "Colouring" True [fh1, fh2]
fc <- frameNewWithContents "Coordinates" True [fc1, fc2, fc3]
v <- vBoxNew False spacing
mapM_ (boxPackStartDefaults v) [fc, fa, fe, fh]
h <- hBoxNew False spacing
boxPackStartDefaults h fb
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
{ dStatus = dStatus'
, bHome = bHome'
, bLoad = bLoad'
, bSave = bSave'
, bAddressToCoordinates = bAddressToCoordinates'
, bAddressToIslandChild = bAddressToIslandChild'
, bIslandChildToAddress = bIslandChildToAddress'
, bAddressToAngles = bAddressToAngles'
, bIslandToAngles = bIslandToAngles'
, bLowerAngleToAddress = bLowerAngleToAddress'
, bUpperAngleToAddress = bUpperAngleToAddress'
, bPeriodScan = bPeriodScan'
, eAddress = eAddress'
, eIsland = eIsland'
, eChild = eChild'
, eLowerAngle = eLowerAngle'
, eUpperAngle = eUpperAngle'
, eReal = eReal'
, eImag = eImag'
, eSize = eSize'
, eHueShift = eHueShift'
, eHueScale = eHueScale'
, 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 ()
butJ b a = do
_ <- b g0 `onClicked` (wrapA' g0 gR a upI)
return ()
ent e a = do
_ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a)
return ()
entI e a = do
_ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a >> upI)
return ()
upI = do
g <- readIORef gR
iUpdate (gReal g) (gImag g) (gSize g) (gHueShift g) (gHueScale g)
cUpdate (gReal g) (gImag g) (gSize g)
aUpdate re im z = do
atomicModifyIORef gR $ \g ->
( g{ gReal = Just re, gImag = Just im, gSize = Just z }, () )
g <- readIORef gR
uReal g0 g
uImag g0 g
uSize g0 g
cUpdate (gReal g) (gImag g) (gSize g)
butI bHome aHome
butI bLoad aLoad
but bSave aSave
butJ bAddressToCoordinates aAddressToCoordinates
but bAddressToIslandChild aAddressToIslandChild
but bIslandChildToAddress aIslandChildToAddress
but bAddressToAngles aAddressToAngles
but bIslandToAngles aIslandToAngles
but bLowerAngleToAddress aLowerAngleToAddress
but bUpperAngleToAddress aUpperAngleToAddress
butJ bPeriodScan aPeriodScan
ent eAddress aAddress
ent eIsland aIsland
ent eChild aChild
ent eLowerAngle aLowerAngle
ent eUpperAngle aUpperAngle
entI eReal aReal
entI eImag aImag
entI eSize aSize
entI eHueShift aHueShift
entI eHueScale aHueScale
let aExit' = (exit (Log.log lg) stateFile =<< readIORef gR)
iInitializeLate aUpdate aExit'
cInitializeLate aExit'
_ <- ww `onDestroy` aExit'
g <- readIORef gR
uEverything g0 g
upI
refreshGUI g0 g
widgetShowAll ww
widgetShowAll cw
widgetShowAll iw
mainGUI
data Gruff = Gruff
{ gAddress :: Maybe AngledInternalAddress
, gIsland :: Maybe AngledInternalAddress
, gChild :: Maybe [Angle]
, gLowerAngle :: Maybe Angle
, gUpperAngle :: Maybe Angle
, gReal :: Maybe QuadDouble
, gImag :: Maybe QuadDouble
, gSize :: Maybe QuadDouble
} | Gruff1
{ gAddress :: Maybe AngledInternalAddress
, gIsland :: Maybe AngledInternalAddress
, gChild :: Maybe [Angle]
, gLowerAngle :: Maybe Angle
, gUpperAngle :: Maybe Angle
, gReal :: Maybe QuadDouble
, gImag :: Maybe QuadDouble
, gSize :: Maybe QuadDouble
, gHueShift :: Maybe Double
, gHueScale :: Maybe Double
}
deriving (Read, Show)
initialGruff :: Gruff
initialGruff = Gruff1
{ gAddress = parse "1"
, gIsland = parse "1"
, gChild = Just []
, gLowerAngle = Just 0
, gUpperAngle = Just 1
, gReal = Just 0
, gImag = Just 0
, gSize = Just 1
, gHueShift = Just 0
, gHueScale = Just 1
}
refreshGUI :: GruffGUI -> Gruff -> IO ()
refreshGUI g0 g = do
can bAddressToCoordinates $ j gAddress
can bAddressToIslandChild $ j gAddress
can bIslandChildToAddress $ j gIsland && j gChild
can bAddressToAngles $ j gAddress
can bIslandToAngles $ j gIsland
can bLowerAngleToAddress $ j gLowerAngle
can bUpperAngleToAddress $ j gUpperAngle
can bPeriodScan $ True
where
can w = widgetSetSensitive (w g0)
j a = isJust (a g)
-- button actions
type A = GruffGUI -> Gruff -> IO Gruff
type A' = GruffGUI -> Gruff -> (Gruff -> IO ()) -> IO ()
wrapA :: GruffGUI -> IORef Gruff -> A -> IO ()
wrapA g0 gR a = do
g <- readIORef gR
g' <- a g0 g
writeIORef gR $! g'
refreshGUI g0 g'
wrapA' :: GruffGUI -> IORef Gruff -> A' -> IO () -> IO ()
wrapA' g0 gR a upI = do
g <- readIORef gR
a g0 g $ \g' -> postGUISync $ do
writeIORef gR $! g'
upI
refreshGUI g0 g'
aHome :: A
aHome g0 _ = do
let g = initialGruff
uEverything g0 g
return g
aDoLoad :: FilePath -> IO (Maybe Gruff)
aDoLoad ff = (do
gr <- safeRead `fmap` readFile ff
return $ case gr of
Just (Gruff a b c d e f g h) -> Just (Gruff1 a b c d e f g h (Just 0) (Just 1))
g -> g
) `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
aPeriodScan :: A'
aPeriodScan g0 g gn = do
statusDialog (dStatus g0) "gruff status" $ \progress -> do
progress "Scanning for period..."
let ps = do
re <- gReal g
im <- gImag g
r <- gSize g
let c = re :+ im
p <- periodScan r c
return (p, r, c)
g' <- case ps of
Just (p, r, c) -> do
progress $ "Scanning for nucleus (" ++ show p ++ ")..."
case periodNucleus p r c of
Just n -> do
progress $ "Computing nucleus (" ++ show p ++ ")..."
case refineNucleus p n of
(z, (re :+ im)) | 10 > z && z > 0 ->
return g{ gReal = Just re, gImag = Just im, gSize = Just (z * 4) }
_ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
_ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
_ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
progress "Done!"
postGUISync $ do
uReal g0 g'
uImag g0 g'
uSize g0 g'
gn g'
aAddressToCoordinates :: A'
aAddressToCoordinates g0 g gn = do
statusDialog (dStatus g0) "gruff status" $ \progress -> do
progress "Splitting address..."
g1 <- aAddressToIslandChild g0 g
progress "Finding external angles..."
g2 <- aIslandToAngles g0 g1
progress "Tracing external rays..."
g' <- aAnglesChildToCoordinates (progress "Computing nucleus...") g0 g2
progress "Done!"
postGUISync $ do
uReal g0 g'
uImag g0 g'
uSize g0 g'
gn g'
aAnglesChildToCoordinates :: IO () -> A
aAnglesChildToCoordinates progress _g0 g = do
case liftM2 (,) (gIsland g) (gLowerAngle g) of
Just (a, lo) ->
let eps = 2 ** negate 48
eps' = 2 ** negate 40
p = addressPeriod a
rlo = externalRay eps 8 (2**24) lo
ok' w = not (nanTest w || infTest w)
ok (w:+x) = ok' w && ok' x
converge [] = Nothing
converge [x] = Just x
converge (x:m@(y:_))
| not $ magnitude (x - y) < eps' = converge m
| otherwise = Just x
rend = converge . takeWhile ok $ rlo
in case rend of
Just c -> progress >> case refineNucleus p c of
(z, (re :+ im)) | z > 0 ->
return g{ gReal = Just re, gImag = Just im, gSize = Just (z * 4) }
_ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
Nothing -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
Nothing -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
aAddressToIslandChild :: A
aAddressToIslandChild g0 g = do
let g' = fromMaybe g{ gIsland = Nothing, gChild = Nothing } $ do
a <- gAddress g
(i, c) <- splitAddress a
return g{ gIsland = Just i, gChild = Just c }
postGUISync $ uIsland g0 g'
postGUISync $ uChild g0 g'
return g'
aIslandChildToAddress :: A
aIslandChildToAddress g0 g = do
let g' = g{ gAddress = liftM2 joinAddress (gIsland g) (gChild g) }
postGUISync $ uAddress g0 g'
return g'
aAddressToAngles :: A
aAddressToAngles g0 g = do
let (l, u) = case externalAngles =<< gAddress g of
Just (lo, up) -> (Just lo, Just up)
Nothing -> (Nothing, Nothing)
g' = g{ gLowerAngle = l, gUpperAngle = u }
postGUISync $ uLowerAngle g0 g'
postGUISync $ uUpperAngle g0 g'
return g'
aIslandToAngles :: A
aIslandToAngles g0 g = do
let (l, u) = case externalAngles =<< gIsland g of
Just (lo, up) -> (Just lo, Just up)
Nothing -> (Nothing, Nothing)
g' = g{ gLowerAngle = l, gUpperAngle = u }
-- strictness hack to prevent evaluation in gui thread...
case (gLowerAngle g', gUpperAngle g') of
(Just lo, Just up) | lo + up > 0 -> return ()
_ -> return ()
postGUISync $ uLowerAngle g0 g'
postGUISync $ uUpperAngle g0 g'
return g'
aLowerAngleToAddress :: A
aLowerAngleToAddress g0 g = do
let a = angledInternalAddress =<< gLowerAngle g
g' = g{ gAddress = a }
postGUISync $ uAddress g0 g'
return g'
aUpperAngleToAddress :: A
aUpperAngleToAddress g0 g = do
let a = angledInternalAddress =<< gUpperAngle g
g' = g{ gAddress = a }
postGUISync $ uAddress g0 g'
return g'
-- entry update
type U = GruffGUI -> Gruff -> IO ()
uEverything, uAddress, uIsland, uChild, uLowerAngle, uUpperAngle, uReal, uImag, uSize :: U
uEverything g0 g = forM_ [uAddress, uIsland, uChild, uLowerAngle, uUpperAngle, uReal, uImag, uSize] $ \u -> u g0 g
uAddress g0 g = do
let s = filter ('"'/=) . show . pretty
entrySetText (eAddress g0) (maybe "" s (gAddress g))
uIsland g0 g = do
let s = filter ('"'/=) . show . pretty
entrySetText (eIsland g0) (maybe "" s (gIsland g))
uChild g0 g = do
let s x = show (numerator x) ++ "/" ++ show (denominator x)
t = unwords . map s
entrySetText (eChild g0) (maybe "" t (gChild g))
uLowerAngle g0 g = do
let s x = show (numerator x) ++ " / " ++ show (denominator x)
entrySetText (eLowerAngle g0) (maybe "" s (gLowerAngle g))
uUpperAngle g0 g = do
let s x = show (numerator x) ++ " / " ++ show (denominator x)
entrySetText (eUpperAngle g0) (maybe "" s (gUpperAngle g))
uReal g0 g = entrySetText (eReal g0) (maybe "" show (gReal g))
uImag g0 g = entrySetText (eImag g0) (maybe "" show (gImag g))
uSize g0 g = entrySetText (eSize g0) (maybe "" show (gSize g))
-- 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'
refreshGUI g0 g'
aAddress, aIsland, aChild, aLowerAngle, aUpperAngle, aReal, aImag, aSize, aHueShift, aHueScale :: E
aAddress g s = g{ gAddress = parse s }
aIsland g s = g{ gIsland = parse s }
aChild g s = g{ gChild = parseAngles s }
aLowerAngle g s = g{ gLowerAngle = parseAngle s }
aUpperAngle g s = g{ gUpperAngle = parseAngle s }
aReal g s = g{ gReal = safeRead s }
aImag g s = g{ gImag = safeRead s }
aSize g s = g{ gSize = safeRead s }
aHueShift g s = g{ gHueShift = safeRead s }
aHueScale g s = g{ gHueScale = safeRead s }
minSize :: Size
minSize = Size 160 100