{- | A Bravura font handle with a glyph rasterization cache.

Wraps the raw "Termtab.Graphics.FreeType" FFI: loads the face once and memoizes
rendered glyphs by @(pixel-size, char)@ so the notation renderer never
re-rasterizes the same SMuFL glyph across frames. Failures (glyph not found)
are cached too, so a bad code point is only attempted once.
-}
module Termtab.Graphics.Font (
    GlyphFont,
    bravuraFontPath,
    openGlyphFont,
    withGlyphFont,
    closeGlyphFont,
    cachedGlyph,
) where

import Control.Exception (bracket)
import Data.IORef
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import System.Environment (lookupEnv)

import Termtab.Graphics.FreeType

{- | An open font face plus its memoized glyph cache, keyed by
(pixel size, character) in 'cachedGlyph'.
-}
data GlyphFont = GlyphFont
    { GlyphFont -> FontFace
gfFace :: FontFace
    , GlyphFont -> IORef (Map (Int, Char) (Maybe GlyphBitmap))
gfCache :: IORef (Map (Int, Char) (Maybe GlyphBitmap))
    }

-- | The configured Bravura font path (@TERMTAB_BRAVURA_FONT@), if any.
bravuraFontPath :: IO (Maybe FilePath)
bravuraFontPath :: IO (Maybe FilePath)
bravuraFontPath = FilePath -> IO (Maybe FilePath)
lookupEnv FilePath
"TERMTAB_BRAVURA_FONT"

-- | Load a font face and start an empty glyph cache.
openGlyphFont :: FilePath -> IO GlyphFont
openGlyphFont :: FilePath -> IO GlyphFont
openGlyphFont FilePath
path = do
    face <- FilePath -> IO FontFace
newFontFace FilePath
path
    ref <- newIORef Map.empty
    pure (GlyphFont face ref)

-- | Release the underlying face.
closeGlyphFont :: GlyphFont -> IO ()
closeGlyphFont :: GlyphFont -> IO ()
closeGlyphFont = FontFace -> IO ()
freeFontFace (FontFace -> IO ())
-> (GlyphFont -> FontFace) -> GlyphFont -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. GlyphFont -> FontFace
gfFace

-- | Bracketed 'openGlyphFont' / 'closeGlyphFont'.
withGlyphFont :: FilePath -> (GlyphFont -> IO a) -> IO a
withGlyphFont :: forall a. FilePath -> (GlyphFont -> IO a) -> IO a
withGlyphFont FilePath
path = IO GlyphFont -> (GlyphFont -> IO ()) -> (GlyphFont -> IO a) -> IO a
forall a b c. IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket (FilePath -> IO GlyphFont
openGlyphFont FilePath
path) GlyphFont -> IO ()
closeGlyphFont

{- | Rasterize a glyph at the given pixel size, returning a cached result on
repeat calls. 'Nothing' means FreeType could not render the code point.
-}
cachedGlyph :: GlyphFont -> Int -> Char -> IO (Maybe GlyphBitmap)
cachedGlyph :: GlyphFont -> Int -> Char -> IO (Maybe GlyphBitmap)
cachedGlyph GlyphFont
gf Int
size Char
ch = do
    cache <- IORef (Map (Int, Char) (Maybe GlyphBitmap))
-> IO (Map (Int, Char) (Maybe GlyphBitmap))
forall a. IORef a -> IO a
readIORef (GlyphFont -> IORef (Map (Int, Char) (Maybe GlyphBitmap))
gfCache GlyphFont
gf)
    case Map.lookup (size, ch) cache of
        Just Maybe GlyphBitmap
cached -> Maybe GlyphBitmap -> IO (Maybe GlyphBitmap)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe GlyphBitmap
cached
        Maybe (Maybe GlyphBitmap)
Nothing -> do
            rendered <- FontFace -> Int -> Char -> IO (Maybe GlyphBitmap)
renderGlyph (GlyphFont -> FontFace
gfFace GlyphFont
gf) Int
size Char
ch
            modifyIORef' (gfCache gf) (Map.insert (size, ch) rendered)
            pure rendered