From 45905aa718a0aae54c297f44379ceed03296d6db Mon Sep 17 00:00:00 2001 From: Schell Scivally Date: Sat, 29 Sep 2018 10:47:40 -0700 Subject: [PATCH] first commit --- .gitignore | 20 +++ ChangeLog.md | 3 + LICENSE | 30 ++++ README.md | 6 + Setup.hs | 2 + app/Main.hs | 130 ++++++++++++++ assets/Neuton-Regular.ttf | Bin 0 -> 56688 bytes assets/OFL.txt | 94 +++++++++++ package.yaml | 61 +++++++ src/Typograffiti.hs | 124 ++++++++++++++ src/Typograffiti/Atlas.hs | 347 ++++++++++++++++++++++++++++++++++++++ src/Typograffiti/GL.hs | 337 ++++++++++++++++++++++++++++++++++++ src/Typograffiti/Glyph.hs | 30 ++++ src/Typograffiti/Utils.hs | 129 ++++++++++++++ stack.yaml | 65 +++++++ test/Spec.hs | 2 + 16 files changed, 1380 insertions(+) create mode 100644 .gitignore create mode 100644 ChangeLog.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 app/Main.hs create mode 100644 assets/Neuton-Regular.ttf create mode 100644 assets/OFL.txt create mode 100644 package.yaml create mode 100644 src/Typograffiti.hs create mode 100644 src/Typograffiti/Atlas.hs create mode 100644 src/Typograffiti/GL.hs create mode 100644 src/Typograffiti/Glyph.hs create mode 100644 src/Typograffiti/Utils.hs create mode 100644 stack.yaml create mode 100644 test/Spec.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27b9e2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +dist* +*.hi +*.o +*.sqlite3 +.cabal-sandbox +cabal.sandbox.config +cabal.config +*/.stack-work/ +.stack-work/ +*.sw[a-z] +*.hp +*.prof +*.tags +*.out +*.tmp +.DS_Store +.projectile +TAGS +*.#* +*.cabal diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..6813757 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,3 @@ +# Changelog for typograffiti + +## Unreleased changes diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e037c72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2018 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..742fdd2 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# typograffiti +Typograffiti aims to make working with text in multimedia applications easy. + +## requirements +* opengl 3.x +* freetype 2.x diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..d7dd569 --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,130 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +module Main where + +import Control.Monad (forever) +import Control.Monad.Except (runExceptT, withExceptT) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Vector.Unboxed as UV +import Graphics.GL +import SDL +import System.FilePath (()) +import Typograffiti +import Typograffiti.GL + + +vertexShader :: ByteString +vertexShader = B8.pack $ unlines + [ "#version 330 core" + , "uniform mat4 projection;" + , "uniform mat4 modelview;" + , "in vec2 position;" + , "in vec2 uv;" + , "out vec2 fuv;" + , "void main () {" + , " fuv = uv;" + , " gl_Position = projection * modelview * vec4(position, 0.0, 0.1);" + , "}" + ] + + +fragmentShader :: ByteString +fragmentShader = B8.pack $ unlines + [ "#version 330 core" + , "in vec2 fuv;" + , "out vec4 fcolor;" + , "uniform sampler2D tex;" + , "void main () {" + , " fcolor = texture(tex, fuv);" + , "}" + ] + + +main :: IO () +main = do + SDL.initializeAll + + let openGL = defaultOpenGL + { glProfile = Core Debug 3 3 } + wcfg = defaultWindow + { windowInitialSize = V2 640 480 + , windowOpenGL = Just openGL + } + + w <- createWindow "Typograffiti" wcfg + _ <- glCreateContext w + let ttfName = "assets" "Neuton-Regular.ttf" + + (either fail return =<<) . runExceptT $ do + -- Get the atlas + atlas <- withExceptT show + $ allocAtlas ttfName (PixelSize 16 16) asciiChars + -- Compile our shader program + let position = 0 + uv = 1 + vert <- compileOGLShader vertexShader GL_VERTEX_SHADER + frag <- compileOGLShader fragmentShader GL_FRAGMENT_SHADER + prog <- compileOGLProgram + [ ("position", fromIntegral position) + , ("uv", fromIntegral uv) + ] + [vert, frag] + glUseProgram prog + -- Get our uniform locations + projection <- getUniformLocation prog "projection" + modelview <- getUniformLocation prog "modelview" + tex <- getUniformLocation prog "tex" + -- Generate our string geometry + geom <- withExceptT show + $ stringTris atlas True "Hi there" + let (ps, uvs) = UV.unzip geom + -- Buffer the geometry into our attributes + textVao <- withVAO $ \vao -> do + withBuffers 2 $ \[pbuf, uvbuf] -> do + bufferGeometry position pbuf ps + bufferGeometry uv uvbuf uvs + return vao + atlasVao <- withVAO $ \vao -> do + withBuffers 2 $ \[pbuf, uvbuf] -> do + let V2 w h = fromIntegral + <$> atlasTextureSize atlas + bufferGeometry position pbuf $ UV.fromList + [ V2 0 0, V2 w 0, V2 w h + , V2 0 0, V2 w h, V2 0 h + ] + bufferGeometry uv uvbuf $ UV.fromList + [ V2 0 0, V2 1 0, V2 1 1 + , V2 0 0, V2 1 1, V2 0 1 + ] + return vao + + -- Set our model view transform + let mv :: M44 Float + mv = mat4Translate (V3 10 100 0) + mv2 :: M44 Float + mv2 = mv !*! mat4Scale (V3 0.125 0.125 1) + -- Forever loop, drawing stuff + forever $ do + _ <- pollEvents + pj :: M44 Float <- + orthoProjection <$> get (windowSize w) + withBoundTextures [atlasTexture atlas] $ do + updateUniform prog projection pj + updateUniform prog modelview mv + updateUniform prog tex (0 :: Int) + drawVAO + prog + textVao + GL_TRIANGLES + (fromIntegral $ UV.length ps) + + updateUniform prog modelview mv2 + drawVAO + prog + atlasVao + GL_TRIANGLES + 6 + glSwapWindow w diff --git a/assets/Neuton-Regular.ttf b/assets/Neuton-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d856dc43f4d92d8750efaf14b410622e6f708377 GIT binary patch literal 56688 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 + +dependencies: +- base >= 4.7 && < 5 +- bytestring +- containers +- freetype2 +- gl +- linear +- mtl +- template-haskell +- vector + +library: + source-dirs: src + +executables: + typograffiti-exe: + main: Main.hs + source-dirs: app + other-modules: Paths_typograffiti + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - filepath + - JuicyPixels + - sdl2 + - typograffiti + + +tests: + typograffiti-test: + main: Spec.hs + source-dirs: test + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - typograffiti diff --git a/src/Typograffiti.hs b/src/Typograffiti.hs new file mode 100644 index 0000000..b3604e9 --- /dev/null +++ b/src/Typograffiti.hs @@ -0,0 +1,124 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} +-- | +-- Module: Gelatin.FreeType2 +-- Copyright: (c) 2017 Schell Scivally +-- License: MIT +-- Maintainer: Schell Scivally +-- +-- This module provides easy freetype2 font rendering using gelatin's +-- graphics primitives. +-- +module Typograffiti + ( allocAtlas + , GlyphSize (..) + , TypograffitiError (..) + , Atlas (..) + , asciiChars + , stringTris + ) where + +import Typograffiti.Atlas +import Typograffiti.Glyph + + +-------------------------------------------------------------------------------- +-- WordMap +-------------------------------------------------------------------------------- + + +-------------------------------------------------------------------------------- +-- Picture +-------------------------------------------------------------------------------- +-- | Constructs a 'TexturePictureT' of one word in all red. +-- V4ization can then be done using 'setReplacementV4' in the picture +-- computation, or by using 'redChannelReplacement' and passing that to the +-- renderer after compilation, at render time. Keep in mind that any new word +-- geometry will be discarded, since this computation does not return a new 'Atlas'. +-- For that reason it is advised that you load the needed words before using this +-- function. For loading words, see 'loadWords'. +-- +-- This is used in 'freetypeFontRendering' to construct the geometry of each word. +-- 'freetypeFontRendering' goes further and stores these geometries, looking them up +-- and constructing a string of word renderers for each input 'String'. +--freetypePicture +-- :: MonadIO m +-- => Atlas +-- -- ^ The 'Atlas' from which to read font textures word geometry. +-- -> String +-- -- ^ The word to render. +-- -> m FontRendering +-- -- ^ Returns a textured picture computation representing the texture and +-- -- geometry of the input word. +--freetypePicture atlas@Atlas{..} str = do +-- eKerning <- withFreeType (Just atlasLibrary) $ hasKerning atlasFontFace +-- setTextures [atlasTexture] +-- let useKerning = either (const False) id eKerning +-- setGeometry $ triangles $ stringTris atlas useKerning str +-------------------------------------------------------------------------------- +-- Performance FontRendering +-------------------------------------------------------------------------------- +-- | Constructs a 'FontRendering' from the given color and string. The 'WordMap' +-- record of the given 'Atlas' is used to construct the string geometry, greatly +-- improving performance and allowing longer strings to be compiled and renderered +-- in real time. To create a new 'Atlas' see 'allocAtlas'. +-- +-- Note that since word geometries are stored in the 'Atlas' 'WordMap' and multiple +-- renderers can reference the same 'Atlas', the returned 'FontRendering' contains a +-- clean up operation that does nothing. It is expected that the programmer +-- will call 'freeAtlas' manually when the 'Atlas' is no longer needed. +--freetypeFontRendering +-- :: MonadIO m +-- => SomeProgram +-- -- ^ The V2(backend, to) use for compilation. +-- -> Atlas +-- -- ^ The 'Atlas' to read character textures from and load word geometry +-- -- into. +-- -> V4 Float +-- -- ^ The solid color to render the string with. +-- -> String +-- -- ^ The string to render. +-- -- This string can contain newlines, which will be respected. +-- -> m (FontRendering, V2 Float, Atlas) +-- -- ^ Returns the 'FontRendering', the size of the text and the new +-- -- 'Atlas' with the loaded geometry of the string. +--freetypeFontRendering b atlas0 color str = do +-- atlas <- loadWords b atlas0 str +-- let glyphw = glyphWidth $ atlasGlyphSize atlas +-- spacew = fromMaybe glyphw $ do +-- metrcs <- IM.lookup (fromEnum ' ') $ atlasMetrics atlas +-- let (x, _) = glyphAdvance metrcs +-- return $ fromIntegral x +-- glyphh = glyphHeight $ atlasGlyphSize atlas +-- spaceh = glyphh +-- isWhiteSpace c = c == ' ' || c == '\n' || c == '\t' +-- renderWord :: [FontTransform] -> V2 Float -> String -> IO () +-- renderWord _ _ "" = return () +-- renderWord rs (V2 _ y) ('\n':cs) = renderWord rs (V2 0 (y + spaceh)) cs +-- renderWord rs (V2 x y) (' ':cs) = renderWord rs (V2 (x + spacew) y) cs +-- renderWord rs (V2 x y) cs = do +-- let word = takeWhile (not . isWhiteSpace) cs +-- rest = drop (length word) cs +-- case M.lookup word (atlasWordMap atlas) of +-- Nothing -> renderWord rs (V2 x y) rest +-- Just (V2 w _, r) -> do +-- let ts = [move x y, redChannelReplacementV4 color] +-- snd r $ ts ++ rs +-- renderWord rs (V2 (x + w) y) rest +-- rr t = renderWord t 0 str +-- measureString :: (V2 Float, V2 Float) -> String -> (V2 Float, V2 Float) +-- measureString (V2 x y, V2 w h) "" = (V2 x y, V2 w h) +-- measureString (V2 x y, V2 w _) (' ':cs) = +-- let nx = x + spacew in measureString (V2 nx y, V2 (max w nx) y) cs +-- measureString (V2 x y, V2 w h) ('\n':cs) = +-- let ny = y + spaceh in measureString (V2 x ny, V2 w (max h ny)) cs +-- measureString (V2 x y, V2 w h) cs = +-- let word = takeWhile (not . isWhiteSpace) cs +-- rest = drop (length word) cs +-- n = case M.lookup word (atlasWordMap atlas) of +-- Nothing -> (V2 x y, V2 w h) +-- Just (V2 ww _, _) -> let nx = x + ww +-- in (V2 nx y, V2 (max w nx) y) +-- in measureString n rest +-- (szw, szh) = snd $ measureString (0,0) str +-- return ((return (), rr), V2 szw (max spaceh szh), atlas) diff --git a/src/Typograffiti/Atlas.hs b/src/Typograffiti/Atlas.hs new file mode 100644 index 0000000..f71f7c3 --- /dev/null +++ b/src/Typograffiti/Atlas.hs @@ -0,0 +1,347 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} +-- | +-- Module: Typograffiti.Atlas +-- Copyright: (c) 2018 Schell Scivally +-- License: MIT +-- Maintainer: Schell Scivally +-- +-- This module provides easy freetype2 font rendering without having to mess with +-- opengl. +-- +module Typograffiti.Atlas where + +import Control.Monad +import Control.Monad.Except (MonadError (..)) +import Control.Monad.IO.Class +import Data.IntMap (IntMap) +import qualified Data.IntMap as IM +import Data.Map (Map) +import qualified Data.Map as M +import Data.Vector.Unboxed (Vector) +import qualified Data.Vector.Unboxed as UV +import Foreign.Marshal.Utils (with) +import Graphics.GL.Core32 +import Graphics.GL.Types +import Graphics.Rendering.FreeType.Internal.Bitmap as BM +import Graphics.Rendering.FreeType.Internal.GlyphMetrics as GM +import Linear + +import Typograffiti.GL +import Typograffiti.Glyph +import Typograffiti.Utils + + +data TypograffitiError = + TypograffitiErrorNoGlyphMetricsForChar Char + -- ^ The are no glyph metrics for this character. This probably means + -- the character has not been loaded into the atlas. + | TypograffitiErrorFreetype String String + -- ^ There was a problem while interacting with the freetype2 library. + deriving (Show, Eq) + + +data SpatialTransform = SpatialTransformTranslate (V2 Float) + | SpatialTransformScale (V2 Float) + | SpatialTransformRotate Float + + +data FontTransform = FontTransformAlpha Float + | FontTransformMultiply (V4 Float) + | FontTransformReplaceRed (V4 Float) + | FontTransformSpatial SpatialTransform + + +data FontRendering = FontRendering + { fontRenderingDraw :: [FontTransform] -> IO () + , fontRenderingRelease :: IO () + , fontRenderingSize :: V2 Int + } + + +type WordMap = Map String (V2 Float, FontRendering) + + +-------------------------------------------------------------------------------- +-- Atlas +-------------------------------------------------------------------------------- + + +data Atlas = Atlas { atlasTexture :: GLuint + , atlasTextureSize :: V2 Int + , atlasLibrary :: FT_Library + , atlasFontFace :: FT_Face + , atlasMetrics :: IntMap GlyphMetrics + , atlasGlyphSize :: GlyphSize + , atlasFilePath :: FilePath + } + + +emptyAtlas :: FT_Library -> FT_Face -> GLuint -> Atlas +emptyAtlas lib fce t = Atlas t 0 lib fce mempty (PixelSize 0 0) "" + + +data AtlasMeasure = AM { amWH :: V2 Int + , amXY :: V2 Int + , rowHeight :: Int + , amMap :: IntMap (V2 Int) + } deriving (Show, Eq) + + +emptyAM :: AtlasMeasure +emptyAM = AM 0 (V2 1 1) 0 mempty + + +spacing :: Int +spacing = 1 + + +measure :: FT_Face -> Int -> AtlasMeasure -> Char -> FreeTypeIO AtlasMeasure +measure fce maxw am@AM{..} char + | Just _ <- IM.lookup (fromEnum char) amMap = return am + | otherwise = do + let V2 x y = amXY + V2 w h = amWH + -- Load the char, replacing the glyph according to + -- https://www.freetype.org/freetype2/docs/tutorial/step1.html + loadChar fce (fromIntegral $ fromEnum char) ft_LOAD_RENDER + -- Get the glyph slot + slot <- liftIO $ peek $ glyph fce + -- Get the bitmap + bmp <- liftIO $ peek $ bitmap slot + let bw = fromIntegral $ BM.width bmp + bh = fromIntegral $ rows bmp + gotoNextRow = (x + bw + spacing) >= maxw + rh = if gotoNextRow then 0 else max bh rowHeight + nx = if gotoNextRow then 0 else x + bw + spacing + nw = max w (x + bw + spacing) + nh = max h (y + rh + spacing) + ny = if gotoNextRow then nh else y + am1 = AM { amWH = V2 nw nh + , amXY = V2 nx ny + , rowHeight = rh + , amMap = IM.insert (fromEnum char) amXY amMap + } + return am1 + + +texturize :: IntMap (V2 Int) -> Atlas -> Char -> FreeTypeIO Atlas +texturize xymap atlas@Atlas{..} char + | Just pos@(V2 x y) <- IM.lookup (fromEnum char) xymap = do + -- Load the char + loadChar atlasFontFace (fromIntegral $ fromEnum char) ft_LOAD_RENDER + -- Get the slot and bitmap + slot <- liftIO $ peek $ glyph atlasFontFace + bmp <- liftIO $ peek $ bitmap slot + -- Update our texture by adding the bitmap + glTexSubImage2D GL_TEXTURE_2D 0 + (fromIntegral x) (fromIntegral y) + (fromIntegral $ BM.width bmp) (fromIntegral $ rows bmp) + GL_RED GL_UNSIGNED_BYTE + (castPtr $ buffer bmp) + -- Get the glyph metrics + ftms <- liftIO $ peek $ metrics slot + -- Add the metrics to the atlas + let vecwh = fromIntegral <$> V2 (BM.width bmp) (rows bmp) + canon = floor @Double @Int . (* 0.5) . (* 0.015625) . fromIntegral + vecsz = canon <$> V2 (GM.width ftms) (GM.height ftms) + vecxb = canon <$> V2 (horiBearingX ftms) (horiBearingY ftms) + vecyb = canon <$> V2 (vertBearingX ftms) (vertBearingY ftms) + vecad = canon <$> V2 (horiAdvance ftms) (vertAdvance ftms) + mtrcs = GlyphMetrics { glyphTexBB = (pos, pos + vecwh) + , glyphTexSize = vecwh + , glyphSize = vecsz + , glyphHoriBearing = vecxb + , glyphVertBearing = vecyb + , glyphAdvance = vecad + } + return atlas{ atlasMetrics = IM.insert (fromEnum char) mtrcs atlasMetrics } + + | otherwise = do + liftIO $ putStrLn "could not find xy" + return atlas + +-- | Allocate a new 'Atlas'. +-- When creating a new 'Atlas' you must pass all the characters that you +-- might need during the life of the 'Atlas'. Character texturization only +-- happens here. +allocAtlas + :: ( MonadIO m + , MonadError TypograffitiError m + ) + => FilePath + -- ^ 'FilePath' of the 'Font' to use for this 'Atlas'. + -> GlyphSize + -- ^ Size of glyphs in this 'Atlas' + -> String + -- ^ The characters to include in this 'Atlas'. + -> m Atlas +allocAtlas fontFilePath gs str = do + e <- liftIO $ runFreeType $ do + fce <- newFace fontFilePath + case gs of + PixelSize w h -> setPixelSizes fce (2*w) (2*h) + CharSize w h dpix dpiy -> setCharSize fce (floor $ 26.6 * 2 * w) + (floor $ 26.6 * 2 * h) + dpix dpiy + + AM{..} <- foldM (measure fce 512) emptyAM str + + let V2 w h = amWH + xymap = amMap + + t <- liftIO $ do + t <- allocAndActivateTex GL_TEXTURE0 + glPixelStorei GL_UNPACK_ALIGNMENT 1 + withCString (replicate (w * h) $ toEnum 0) $ + glTexImage2D GL_TEXTURE_2D 0 GL_RED (fromIntegral w) (fromIntegral h) + 0 GL_RED GL_UNSIGNED_BYTE . castPtr + return t + + lib <- getLibrary + atlas <- foldM (texturize xymap) (emptyAtlas lib fce t) str + + glGenerateMipmap GL_TEXTURE_2D + glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_S GL_REPEAT + glTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_T GL_REPEAT + glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR + glTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR + glBindTexture GL_TEXTURE_2D 0 + glPixelStorei GL_UNPACK_ALIGNMENT 4 + return + atlas{ atlasTextureSize = V2 w h + , atlasGlyphSize = gs + , atlasFilePath = fontFilePath + } + + either + (throwError . TypograffitiErrorFreetype "cannot alloc atlas") + (return . fst) + e + + +-- | Releases all resources associated with the given 'Atlas'. +freeAtlas :: MonadIO m => Atlas -> m () +freeAtlas a = liftIO $ do + _ <- ft_Done_FreeType (atlasLibrary a) + -- _ <- unloadMissingWords a "" + with (atlasTexture a) $ \ptr -> glDeleteTextures 1 ptr + + +-- | Load a string of words into the 'Atlas'. +--loadWords +-- :: MonadIO m +-- => _program +-- -- ^ The V2(backend, needed) to render font glyphs. +-- -> Atlas +-- -- ^ The atlas to load the words into. +-- -> String +-- -- ^ The string of words to load, with each word separated by spaces. +-- -> m Atlas +--loadWords b atlas str = do +-- wm <- liftIO $ foldM loadWord (atlasWordMap atlas) $ words str +-- return atlas{atlasWordMap=wm} +-- where loadWord wm word +-- | M.member word wm = return wm +-- | otherwise = do +-- let pic = do freetypePicture atlas word +-- _pictureSize2 fst +-- (sz,r) <- _compilePictureT b pic +-- return $ M.insert word (sz,r) wm + + +-- | Unload any words not contained in the source string. +--unloadMissingWords +-- :: MonadIO m +-- => Atlas +-- -- ^ The 'Atlas' to unload words from. +-- -> String +-- -- ^ The source string. +-- -> m Atlas +--unloadMissingWords atlas str = do +-- let wm = atlasWordMap atlas +-- ws = M.fromList $ zip (words str) [(0::Int)..] +-- missing = M.difference wm ws +-- retain = M.difference wm missing +-- dealoc = liftIO . fontRenderingRelease . snd +-- <$> missing +-- sequence_ dealoc +-- return atlas{atlasWordMap=retain} + + +-- | Construct the geometry needed to render the given character. +makeCharQuad + :: ( MonadIO m + , MonadError TypograffitiError m + ) + => Atlas + -- ^ The atlas that contains the metrics for the given character. + -> Bool + -- ^ Whether or not to use kerning. + -> Int + -- ^ The current "pen position". + -> Maybe FT_UInt + -- ^ The freetype index of the previous character, if available. + -> Char + -- ^ The character to generate geometry for. + -> m (Vector (V2 Float, V2 Float), Int, Maybe FT_UInt) + -- ^ Returns the generated geometry (position in 2-space and UV parameters), + -- the next pen position and the freetype index of the given character, if + -- available. +makeCharQuad Atlas{..} useKerning penx mLast char = do + let ichar = fromEnum char + eNdx <- withFreeType (Just atlasLibrary) $ getCharIndex atlasFontFace ichar + let mndx = either (const Nothing) Just eNdx + px <- case (,,) <$> mndx <*> mLast <*> Just useKerning of + Just (ndx,lndx,True) -> do + e <- withFreeType (Just atlasLibrary) $ + getKerning atlasFontFace lndx ndx ft_KERNING_DEFAULT + return $ either (const penx) ((+penx) . floor . (/(64::Double)) . fromIntegral . fst) e + _ -> return $ fromIntegral penx + case IM.lookup ichar atlasMetrics of + Nothing -> throwError $ TypograffitiErrorNoGlyphMetricsForChar char -- return (penx, mndx) + Just GlyphMetrics{..} -> do + let V2 dx dy = fromIntegral <$> glyphHoriBearing + x = fromIntegral px + dx + y = -dy + V2 w h = fromIntegral <$> glyphSize + V2 aszW aszH = fromIntegral <$> atlasTextureSize + V2 texL texT = fromIntegral <$> fst glyphTexBB + V2 texR texB = fromIntegral <$> snd glyphTexBB + + tl = (V2 x y , V2 (texL/aszW) (texT/aszH)) + tr = (V2 (x+w) y , V2 (texR/aszW) (texT/aszH)) + br = (V2 (x+w) (y+h), V2 (texR/aszW) (texB/aszH)) + bl = (V2 x (y+h), V2 (texL/aszW) (texB/aszH)) + let vs = UV.fromList [ tl, tr, br + , tl, br, bl + ] + let V2 ax _ = glyphAdvance + return (vs, px + ax, mndx) + + +-- | A string containing all standard ASCII characters. +-- This is often passed as the 'String' parameter in 'allocAtlas'. +asciiChars :: String +asciiChars = map toEnum [32..126] + + +-- | Generate the geometry of the given string. +stringTris + :: ( MonadIO m + , MonadError TypograffitiError m + ) + => Atlas + -- ^ The font atlas. + -> Bool + -- ^ Whether or not to use kerning. + -> String + -- ^ The string. + -> m (Vector (V2 Float, V2 Float)) +stringTris atlas useKerning str = do + (vs, _, _) <- foldM gen (mempty, 0, Nothing) str + return $ UV.concat vs + where gen (vs, penx, mndx) c = do + (newVs, newPenx, newMndx) <- makeCharQuad atlas useKerning penx mndx c + return (vs ++ [newVs], newPenx, newMndx) diff --git a/src/Typograffiti/GL.hs b/src/Typograffiti/GL.hs new file mode 100644 index 0000000..39bc5ab --- /dev/null +++ b/src/Typograffiti/GL.hs @@ -0,0 +1,337 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} +module Typograffiti.GL where + +import Control.Exception (assert) +import Control.Monad (forM_, when) +import Control.Monad.Except (MonadError (..)) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Foldable as F +import qualified Data.Vector.Storable as SV +import Data.Vector.Unboxed (Unbox) +import qualified Data.Vector.Unboxed as UV +import Foreign.C.String (peekCAStringLen, withCString) +import Foreign.Marshal.Array +import Foreign.Marshal.Utils +import Foreign.Ptr +import Foreign.Storable +import GHC.TypeLits (KnownNat) +import Graphics.GL.Core32 +import Graphics.GL.Types +import Linear +import Linear.V (Finite, Size, dim, toV) + + + +allocAndActivateTex :: GLenum -> IO GLuint +allocAndActivateTex u = do + [t] <- allocaArray 1 $ \ptr -> do + glGenTextures 1 ptr + peekArray 1 ptr + glActiveTexture u + glBindTexture GL_TEXTURE_2D t + return t + + +clearErrors :: String -> IO () +clearErrors str = do + err' <- glGetError + when (err' /= 0) $ do + putStrLn $ unwords [str, show err'] + assert False $ return () + + +withVAO :: MonadIO m => (GLuint -> IO b) -> m b +withVAO f = liftIO $ do + [vao] <- allocaArray 1 $ \ptr -> do + glGenVertexArrays 1 ptr + peekArray 1 ptr + glBindVertexArray vao + r <- f vao + clearErrors "withVAO" + glBindVertexArray 0 + return r + + +withBuffers :: Int -> ([GLuint] -> IO b) -> IO b +withBuffers n f = do + bufs <- allocaArray n $ \ptr -> do + glGenBuffers (fromIntegral n) ptr + peekArray (fromIntegral n) ptr + f bufs + + +-- | Buffer some geometry into an attribute. +-- The type variable 'f' should be V0, V1, V2, V3 or V4. +bufferGeometry + :: ( Foldable f + , Unbox (f Float) + , Storable (f Float) + , Finite f + , KnownNat (Size f) + ) + => GLuint + -- ^ The attribute location. + -> GLuint + -- ^ The buffer identifier. + -> UV.Vector (f Float) + -- ^ The geometry to buffer. + -> IO () +bufferGeometry loc buf as + | UV.null as = return () + | otherwise = do + let v = UV.head as + asize = UV.length as * sizeOf v + n = fromIntegral $ dim $ toV v + glBindBuffer GL_ARRAY_BUFFER buf + SV.unsafeWith (convertVec as) $ \ptr -> + glBufferData GL_ARRAY_BUFFER (fromIntegral asize) (castPtr ptr) GL_STATIC_DRAW + glEnableVertexAttribArray loc + glVertexAttribPointer loc n GL_FLOAT GL_FALSE 0 nullPtr + clearErrors "bufferGeometry" + + +convertVec + :: (Unbox (f Float), Foldable f) => UV.Vector (f Float) -> SV.Vector GLfloat +convertVec = + SV.convert . UV.map realToFrac . UV.concatMap (UV.fromList . F.toList) + + +-- | Binds the given textures to GL_TEXTURE0, GL_TEXTURE1, ... in ascending +-- order of the texture unit, runs the IO action and then unbinds the textures. +withBoundTextures :: MonadIO m => [GLuint] -> m a -> m a +withBoundTextures ts f = do + liftIO $ mapM_ (uncurry bindTex) (zip ts [GL_TEXTURE0 ..]) + a <- f + liftIO $ glBindTexture GL_TEXTURE_2D 0 + return a + where bindTex tex u = glActiveTexture u >> glBindTexture GL_TEXTURE_2D tex + + +drawVAO + :: MonadIO m + => GLuint + -- ^ The program + -> GLuint + -- ^ The vao + -> GLenum + -- ^ The draw mode + -> GLsizei + -- ^ The number of vertices to draw + -> m () +drawVAO program vao mode num = liftIO $ do + glUseProgram program + glBindVertexArray vao + clearErrors "drawBuffer:glBindVertex" + glDrawArrays mode 0 num + clearErrors "drawBuffer:glDrawArrays" + + +compileOGLShader + :: (MonadIO m, MonadError String m) + => ByteString + -- ^ The shader source + -> GLenum + -- ^ The shader type (vertex, frag, etc) + -> m GLuint + -- ^ Either an error message or the generated shader handle. +compileOGLShader src shType = do + shader <- liftIO $ glCreateShader shType + if shader == 0 + then throwError "Could not create shader" + else do + success <- liftIO $ do + withCString (B8.unpack src) $ \ptr -> + with ptr $ \ptrptr -> glShaderSource shader 1 ptrptr nullPtr + + glCompileShader shader + with (0 :: GLint) $ \ptr -> do + glGetShaderiv shader GL_COMPILE_STATUS ptr + peek ptr + + if success == GL_FALSE + then do + err <- liftIO $ do + infoLog <- with (0 :: GLint) $ \ptr -> do + glGetShaderiv shader GL_INFO_LOG_LENGTH ptr + logsize <- peek ptr + allocaArray (fromIntegral logsize) $ \logptr -> do + glGetShaderInfoLog shader logsize nullPtr logptr + peekArray (fromIntegral logsize) logptr + + return $ unlines [ "Could not compile shader:" + , B8.unpack src + , map (toEnum . fromEnum) infoLog + ] + throwError err + else return shader + + +compileOGLProgram + :: ( MonadIO m + , MonadError String m + ) + => [(String, Integer)] + -> [GLuint] + -> m GLuint +compileOGLProgram attribs shaders = do + (program, success) <- liftIO $ do + program <- glCreateProgram + forM_ shaders (glAttachShader program) + forM_ attribs + $ \(name, loc) -> + withCString name + $ glBindAttribLocation program + $ fromIntegral loc + glLinkProgram program + + success <- with (0 :: GLint) $ \ptr -> do + glGetProgramiv program GL_LINK_STATUS ptr + peek ptr + return (program, success) + + if success == GL_FALSE + then do + err <- liftIO $ with (0 :: GLint) $ \ptr -> do + glGetProgramiv program GL_INFO_LOG_LENGTH ptr + logsize <- peek ptr + infoLog <- allocaArray (fromIntegral logsize) $ \logptr -> do + glGetProgramInfoLog program logsize nullPtr logptr + peekArray (fromIntegral logsize) logptr + return $ unlines [ "Could not link program" + , map (toEnum . fromEnum) infoLog + ] + throwError err + else do + liftIO $ forM_ shaders glDeleteShader + return program + + +orthoProjection + :: Integral a + => V2 a + -- ^ The window width and height + -> M44 Float +orthoProjection (V2 ww wh) = + let (hw,hh) = (fromIntegral ww, fromIntegral wh) + in ortho 0 hw hh 0 0 1 + + +-------------------------------------------------------------------------------- +-- Uniform marshaling functions +-------------------------------------------------------------------------------- + + +getUniformLocation :: MonadIO m => GLuint -> String -> m GLint +getUniformLocation program ident = liftIO + $ withCString ident + $ glGetUniformLocation program + + +class UniformValue a where + updateUniform + :: MonadIO m + => GLuint + -- ^ The program + -> GLint + -- ^ The uniform location + -> a + -- ^ The value. + -> m () + + +clearUniformUpdateError :: Show a => GLuint -> GLint -> a -> IO () +clearUniformUpdateError prog loc val = glGetError >>= \case + 0 -> return () + e -> do + let buf = replicate 256 ' ' + ident <- withCString buf + $ \strptr -> with 0 + $ \szptr -> do + glGetActiveUniformName prog (fromIntegral loc) 256 szptr strptr + sz <- peek szptr + peekCAStringLen (strptr, fromIntegral sz) + putStrLn $ unwords [ "Could not update uniform" + , ident + , "with value" + , show val + , ", encountered error (" ++ show e ++ ")" + , show (GL_INVALID_OPERATION :: Integer, "invalid operation" :: String) + , show (GL_INVALID_VALUE :: Integer, "invalid value" :: String) + ] + assert False $ return () + + +instance UniformValue Bool where + updateUniform p loc bool = liftIO $ do + glUniform1i loc $ if bool then 1 else 0 + clearUniformUpdateError p loc bool + +instance UniformValue Int where + updateUniform p loc enum = liftIO $ do + glUniform1i loc $ fromIntegral $ fromEnum enum + clearUniformUpdateError p loc enum + +instance UniformValue Float where + updateUniform p loc float = liftIO $ do + glUniform1f loc $ realToFrac float + clearUniformUpdateError p loc float + +instance UniformValue Double where + updateUniform p loc d = liftIO $ do + glUniform1f loc $ realToFrac d + clearUniformUpdateError p loc d + +instance UniformValue (V2 Float) where + updateUniform p loc v = liftIO $ do + let V2 x y = fmap realToFrac v + glUniform2f loc x y + clearUniformUpdateError p loc v + +instance UniformValue (V3 Float) where + updateUniform p loc v = liftIO $ do + let V3 x y z = fmap realToFrac v + glUniform3f loc x y z + clearUniformUpdateError p loc v + +instance UniformValue (V4 Float) where + updateUniform p loc v = liftIO $ do + let (V4 r g b a) = realToFrac <$> v + glUniform4f loc r g b a + clearUniformUpdateError p loc v + +instance UniformValue (M44 Float) where + updateUniform p loc val = liftIO $ do + with val $ glUniformMatrix4fv loc 1 GL_TRUE . castPtr + clearUniformUpdateError p loc val + +instance UniformValue (V2 Int) where + updateUniform p loc v = liftIO $ do + let V2 x y = fmap fromIntegral v + glUniform2i loc x y + clearUniformUpdateError p loc v + +instance UniformValue (Int,Int) where + updateUniform p loc = updateUniform p loc . uncurry V2 + + +-------------------------------------------------------------------------------- +-- Matrix helpers +-------------------------------------------------------------------------------- + + +mat4Translate :: Num a => V3 a -> M44 a +mat4Translate = mkTransformationMat identity + +mat4Rotate :: (Num a, Epsilon a, Floating a) => a -> V3 a -> M44 a +mat4Rotate phi v = mkTransformation (axisAngle v phi) (V3 0 0 0) + +mat4Scale :: Num a => V3 a -> M44 a +mat4Scale (V3 x y z) = + V4 (V4 x 0 0 0) + (V4 0 y 0 0) + (V4 0 0 z 0) + (V4 0 0 0 1) diff --git a/src/Typograffiti/Glyph.hs b/src/Typograffiti/Glyph.hs new file mode 100644 index 0000000..f7d31e5 --- /dev/null +++ b/src/Typograffiti/Glyph.hs @@ -0,0 +1,30 @@ +module Typograffiti.Glyph where + + +import Linear + + +data GlyphSize = CharSize Float Float Int Int + | PixelSize Int Int + deriving (Show, Eq, Ord) + + +glyphWidth :: GlyphSize -> Float +glyphWidth (CharSize x y _ _) = if x == 0 then y else x +glyphWidth (PixelSize x y) = fromIntegral $ if x == 0 then y else x + + +glyphHeight :: GlyphSize -> Float +glyphHeight (CharSize x y _ _) = if y == 0 then x else y +glyphHeight (PixelSize x y) = fromIntegral $ if y == 0 then x else y + + +-- | https://www.freetype.org/freetype2/docs/tutorial/step2.html +data GlyphMetrics = GlyphMetrics + { glyphTexBB :: (V2 Int, V2 Int) + , glyphTexSize :: V2 Int + , glyphSize :: V2 Int + , glyphHoriBearing :: V2 Int + , glyphVertBearing :: V2 Int + , glyphAdvance :: V2 Int + } deriving (Show, Eq) diff --git a/src/Typograffiti/Utils.hs b/src/Typograffiti/Utils.hs new file mode 100644 index 0000000..7adfef5 --- /dev/null +++ b/src/Typograffiti/Utils.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TupleSections #-} +module Typograffiti.Utils ( + module FT + , FreeTypeT + , FreeTypeIO + , getAdvance + , getCharIndex + , getLibrary + , getKerning + , glyphFormatString + , hasKerning + , loadChar + , loadGlyph + , newFace + , setCharSize + , setPixelSizes + , withFreeType + , runFreeType +) where + +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Except +import Control.Monad.State.Strict +import Control.Monad (unless) +import Graphics.Rendering.FreeType.Internal as FT +import Graphics.Rendering.FreeType.Internal.PrimitiveTypes as FT +import Graphics.Rendering.FreeType.Internal.Library as FT +import Graphics.Rendering.FreeType.Internal.FaceType as FT +import Graphics.Rendering.FreeType.Internal.Face as FT hiding (generic) +import Graphics.Rendering.FreeType.Internal.GlyphSlot as FT +import Graphics.Rendering.FreeType.Internal.Bitmap as FT +import Graphics.Rendering.FreeType.Internal.Vector as FT +import Foreign as FT +import Foreign.C.String as FT + +-- TODO: Tease out the correct way to handle errors. +-- They're kinda thrown all willy nilly. + +type FreeTypeT m = ExceptT String (StateT FT_Library m) +type FreeTypeIO = FreeTypeT IO + + +glyphFormatString :: FT_Glyph_Format -> String +glyphFormatString fmt + | fmt == ft_GLYPH_FORMAT_COMPOSITE = "ft_GLYPH_FORMAT_COMPOSITE" + | fmt == ft_GLYPH_FORMAT_OUTLINE = "ft_GLYPH_FORMAT_OUTLINE" + | fmt == ft_GLYPH_FORMAT_PLOTTER = "ft_GLYPH_FORMAT_PLOTTER" + | fmt == ft_GLYPH_FORMAT_BITMAP = "ft_GLYPH_FORMAT_BITMAP" + | otherwise = "ft_GLYPH_FORMAT_NONE" + + +liftE :: MonadIO m => String -> IO (Either FT_Error a) -> FreeTypeT m a +liftE msg f = liftIO f >>= \case + Left e -> fail $ unwords [msg, show e] + Right a -> return a + + +runIOErr :: MonadIO m => String -> IO FT_Error -> FreeTypeT m () +runIOErr msg f = do + e <- liftIO f + unless (e == 0) $ fail $ unwords [msg, show e] + + +runFreeType :: MonadIO m => FreeTypeT m a -> m (Either String (a, FT_Library)) +runFreeType f = do + (e,lib) <- liftIO $ alloca $ \p -> do + e <- ft_Init_FreeType p + lib <- peek p + return (e,lib) + if e /= 0 + then do + _ <- liftIO $ ft_Done_FreeType lib + return $ Left $ "Error initializing FreeType2:" ++ show e + else fmap (,lib) <$> evalStateT (runExceptT f) lib + +withFreeType :: MonadIO m => Maybe FT_Library -> FreeTypeT m a -> m (Either String a) +withFreeType Nothing f = runFreeType f >>= \case + Left e -> return $ Left e + Right (a,lib) -> do + _ <- liftIO $ ft_Done_FreeType lib + return $ Right a +withFreeType (Just lib) f = evalStateT (runExceptT f) lib + +getLibrary :: MonadIO m => FreeTypeT m FT_Library +getLibrary = lift get + +newFace :: MonadIO m => FilePath -> FreeTypeT m FT_Face +newFace fp = do + ft <- lift get + liftE "ft_New_Face" $ withCString fp $ \str -> + alloca $ \ptr -> ft_New_Face ft str 0 ptr >>= \case + 0 -> Right <$> peek ptr + e -> return $ Left e + +setCharSize :: (MonadIO m, Integral i) => FT_Face -> i -> i -> i -> i -> FreeTypeT m () +setCharSize ff w h dpix dpiy = runIOErr "ft_Set_Char_Size" $ + ft_Set_Char_Size ff (fromIntegral w) (fromIntegral h) + (fromIntegral dpix) (fromIntegral dpiy) + +setPixelSizes :: (MonadIO m, Integral i) => FT_Face -> i -> i -> FreeTypeT m () +setPixelSizes ff w h = + runIOErr "ft_Set_Pixel_Sizess" $ ft_Set_Pixel_Sizes ff (fromIntegral w) (fromIntegral h) + +getCharIndex :: (MonadIO m, Integral i) + => FT_Face -> i -> FreeTypeT m FT_UInt +getCharIndex ff ndx = liftIO $ ft_Get_Char_Index ff $ fromIntegral ndx + +loadGlyph :: MonadIO m => FT_Face -> FT_UInt -> FT_Int32 -> FreeTypeT m () +loadGlyph ff fg flags = runIOErr "ft_Load_Glyph" $ ft_Load_Glyph ff fg flags + +loadChar :: MonadIO m => FT_Face -> FT_ULong -> FT_Int32 -> FreeTypeT m () +loadChar ff char flags = runIOErr "ft_Load_Char" $ ft_Load_Char ff char flags + +hasKerning :: MonadIO m => FT_Face -> FreeTypeT m Bool +hasKerning = liftIO . ft_HAS_KERNING + +getKerning :: MonadIO m => FT_Face -> FT_UInt -> FT_UInt -> FT_Kerning_Mode -> FreeTypeT m (Int,Int) +getKerning ff prevNdx curNdx flags = liftE "ft_Get_Kerning" $ alloca $ \ptr -> + ft_Get_Kerning ff prevNdx curNdx (fromIntegral flags) ptr >>= \case + 0 -> do FT_Vector vx vy <- peek ptr + return $ Right (fromIntegral vx, fromIntegral vy) + e -> return $ Left e + +getAdvance :: MonadIO m => FT_GlyphSlot -> FreeTypeT m (Int,Int) +getAdvance slot = do + FT_Vector vx vy <- liftIO $ peek $ advance slot + liftIO $ print ("v", vx, vy) + return (fromIntegral vx, fromIntegral vy) diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..361a2b2 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,65 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: lts-12.10 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.7" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor \ No newline at end of file diff --git a/test/Spec.hs b/test/Spec.hs new file mode 100644 index 0000000..cd4753f --- /dev/null +++ b/test/Spec.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Test suite not yet implemented" -- 2.30.2