keid-render-basic-0.1.12.0: src/Resource/Font/Ktxf.hs
module Resource.Font.Ktxf
( Font.Bundle(..)
, load
, Stack(..)
, allocateMonoStacks
, allocateStack1
, shape
, Layout.ShapedText
, place
, Layout.Strategy(..)
, Layout.LineEnd(..)
, PutChar(..)
) where
import RIO
import Geomancy (Vec2, vec2, withVec2)
import Geomancy.Layout.Alignment (Alignment(..))
import Geomancy.Layout.Box (Box(..))
import UnliftIO.Resource (MonadResource)
import UnliftIO.Resource qualified as Resource
import Resource.Source (Source)
import Resource.Source qualified as Source
import Codec.Ktx2.Font (Bundle(..), loadBundleFile, loadBundleBytes)
import Codec.Ktx2.Font qualified as Font
import Codec.Ktx2.Font.Layout qualified as Layout
import Codec.Ktx2.Font.Shaping qualified as Shaping
import Graphics.MSDF.Atlas.Compact qualified as Atlas
-- * Loading
newtype FontError = FontError Text
deriving (Eq, Ord, Show, Generic)
instance Exception FontError
load
:: ( MonadIO m
, MonadReader env m
, HasLogFunc env
, HasCallStack
)
=> Source -> m Bundle
load = \case
Source.File _label path ->
liftIO $ loadBundleFile path
embedded ->
Source.load (liftIO . loadBundleBytes) embedded
-- | A shaping context for one bundle, annotated with its texture id.
data Stack = Stack
{ context :: Font.StackContext Int32
, style :: Layout.TextStyle
}
allocateMonoStacks :: (MonadResource m, Traversable t) => t (Int32, Font.Bundle) -> m (t Stack)
allocateMonoStacks = traverse (uncurry allocateStack1)
allocateStack1 :: MonadResource m => Int32 -> Font.Bundle -> m Stack
allocateStack1 textureId bundle = do
stack <- liftIO $ Font.createStackContext (Identity bundle)
_key <- Resource.register $ Font.destroyStackContext stack
let context = Font.mapWithBundle (Identity (bundle, const textureId)) stack
style <- liftIO $ Layout.bundleStyle context bundle
pure Stack{context, style}
-- * Typesetting
-- BUG: pipeline-specific, should be moved to pipeline models
-- XXX: those can be collapsed to just an index into glyph storage (and the boxes are already storable)
data PutChar = PutChar
{ pcPos :: Vec2 -- quads, static base + dynamic cursor'd positions in ems * shared dynamic scale
, pcSize :: Vec2 -- static base * shared dynamic scale
, pcOffset :: Vec2 -- uv, static
, pcScale :: Vec2
, pcDistanceLower :: Float
, pcDistanceRange :: Float
, pcTextureId :: Int32
} deriving (Show)
-- | Shape and measure the text once, so it can be placed into any box.
shape :: Stack -> Text -> IO Layout.ShapedText
shape Stack{context, style} = Layout.shapeText context style
place
:: Box -- ^ Parent box, screen units
-> Alignment -- ^ Line alignment inside the box, block alignment along it
-> Float -- ^ Line height, in cap-height units
-> Layout.Strategy
-> Float -- ^ Target size, screen units per cap height
-> Stack
-> Layout.ShapedText
-> [PutChar]
place parent (Alignment alignment) lineHeight strategy targetSize Stack{context} shaped =
withVec2 alignment \alignX alignY ->
withVec2 parent.size \parentWidth parentHeight -> do
let
maxWidth = parentWidth / targetSize
placed = Layout.placeText
Layout.LayoutOptions
{ cursor = Shaping.initialCursorDown lineHeight
, strategy
, align = Layout.AlignLeft
}
maxWidth
shaped
-- XXX: ignoring glyph height and using cap height (the sizes are normalized to it)
textHeight = 1 + (fromIntegral (length placed) - 1) * lineHeight
blockOffset =
parent.position
- parent.size * 0.5
+ vec2 0 ((parentHeight - textHeight * targetSize) * alignY + targetSize)
line <- placed
let lineOffset = vec2 ((maxWidth - line.width) * alignX * targetSize) 0
((font, atlas_), glyphs) <- line.runs
(pcTextureId, atlas) <- maybeToList $ (,) <$> Font.lookupBundled font context <*> atlas_
Shaping.PlacedGlyph{glyph = atlasBox, plane = quadBox} <- glyphs
guard $ quadBox.w > 0 && quadBox.h > 0
let
Atlas.Box{x=ax,y=ay,w=aw,h=ah} = atlasBox
pcOffset = vec2 ax ay
pcScale = vec2 aw ah
let
Atlas.Box{x,y,w,h} = quadBox
pcPos = vec2 (x * targetSize) (-y * targetSize) + blockOffset + lineOffset
pcSize = vec2 (w * targetSize) (h * targetSize)
Atlas.Compact
{ _distanceLower = pcDistanceLower
, _distanceRange = pcDistanceRange
} = atlas
pure PutChar{..}