dear-imgui.hs/src/DearImGui/FontAtlas.hs
Mikhail Chekan bb94341ad5
Extended font & glyph support (#114)
* Separate font utils, add glyph support (#113)
* Implement font glyph ranges builder
* Implement raw font config binds
* Implement font atlas module
* Rewrite font altas rebuilder in Managed
2021-12-22 13:28:46 +03:00

502 lines
14 KiB
Haskell

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-|
Module: DearImGui.FontAtlas
Font atlas builder, accompanied with lower-level functions.
@
import qualified DearImGui.FontAtlas as FontAtlas
prepareAtlas =
FontAtlas.rebuild
[ FontAtlas.FileTTF "comic-sans-mono.ttf" 13 csOptions csRanges
, FontAtlas.Default
]
where
csOptions = mconcat
[ FontAtlas.fontNo 1
, FontAtlas.glyphOffset (0, -1)
]
csRanges = RangeBuilder $ mconcat
[ FontAtlas.addText "Hello world"
, FontRanges.addChar 'Ꙑ'
, FontRanges.addRanges FontRanges.Korean
]
@
-}
module DearImGui.FontAtlas
( -- * Main types
Raw.Font(..)
, FontSource(..)
-- * Building atlas
, rebuild
-- ** Configuring sources
, ConfigSetup(..)
, fontDataOwnedByAtlas
, fontNo
, sizePixels
, oversampleH
, oversampleV
, pixelSnapH
, glyphExtraSpacing
, glyphOffset
, glyphRanges
, glyphMinAdvanceX
, glyphMaxAdvanceX
, mergeMode
, fontBuilderFlags
, rasterizerMultiply
, ellipsisChar
-- ** Configuring ranges
, Ranges(..)
, RangesBuilderSetup(..)
, addChar
, addText
, addRanges
, addRangesRaw
, pattern Latin
, pattern Korean
, pattern Japanese
, pattern ChineseFull
, pattern ChineseSimplifiedCommon
, pattern Cyrillic
, pattern Thai
, pattern Vietnamese
-- * Lower level types and functions
-- , Raw.FontConfig(..)
-- , Raw.GlyphRanges(..)
, build
, clear
, setupFont
, setupRanges
, withRanges
, withConfig
, addFontFromFileTTF
, addFontFromFileTTF_
)
where
-- base
import Data.Bool (bool)
import Data.Maybe (fromMaybe)
import Foreign
import Foreign.C
-- transformers
import Control.Monad.IO.Class
( MonadIO, liftIO )
-- managed
import Control.Monad.Managed
( MonadManaged, managed )
import qualified Control.Monad.Managed as Managed
-- unlift
import UnliftIO (MonadUnliftIO)
import UnliftIO.Exception (bracket)
-- dear-imgui
import DearImGui.Raw.Font (Font(..))
import qualified DearImGui.Raw.Font as Raw
import DearImGui.Raw.Font.Config (FontConfig(..))
import qualified DearImGui.Raw.Font.Config as FontConfig
import DearImGui.Raw.Font.GlyphRanges (GlyphRanges(..), GlyphRangesBuilder(..))
import qualified DearImGui.Raw.Font.GlyphRanges as GlyphRanges
import DearImGui.Structs (ImVec2(..), ImWchar)
-- | Font setup data
data FontSource
= DefaultFont
| FromTTF FilePath Float (Maybe ConfigSetup) Ranges
-- TODO: FromMemory
-- | Font config monoid interface to be used in 'FontSource'.
--
-- @
-- mergeMode True <> fontNo 1
-- @
newtype ConfigSetup = ConfigSetup
{ applyToConfig :: FontConfig -> IO ()
}
instance Semigroup ConfigSetup where
ConfigSetup f <> ConfigSetup g =
ConfigSetup \fc -> f fc >> g fc
instance Monoid ConfigSetup where
mempty = ConfigSetup (const mempty)
-- | Glyph ranges settings, from presets to builder configuration.
data Ranges
= RangesRaw GlyphRanges
| RangesBuiltin GlyphRanges.Builtin
| RangesBuilder RangesBuilderSetup
-- | Basic Latin, Extended Latin
pattern Latin :: Ranges
pattern Latin = RangesBuiltin GlyphRanges.Latin
-- | Default + Korean characters
pattern Korean :: Ranges
pattern Korean = RangesBuiltin GlyphRanges.Korean
-- | Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs
pattern Japanese :: Ranges
pattern Japanese = RangesBuiltin GlyphRanges.Japanese
-- | Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs
pattern ChineseFull :: Ranges
pattern ChineseFull = RangesBuiltin GlyphRanges.ChineseFull
-- | Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese
pattern ChineseSimplifiedCommon :: Ranges
pattern ChineseSimplifiedCommon = RangesBuiltin GlyphRanges.ChineseSimplifiedCommon
-- | Default + about 400 Cyrillic characters
pattern Cyrillic :: Ranges
pattern Cyrillic = RangesBuiltin GlyphRanges.Cyrillic
-- | Default + Thai characters
pattern Thai :: Ranges
pattern Thai = RangesBuiltin GlyphRanges.Thai
-- | Default + Vietnamese characters
pattern Vietnamese :: Ranges
pattern Vietnamese = RangesBuiltin GlyphRanges.Vietnamese
-- | Ranges builder monoid interface to be executed through 'buildRanges'.
--
-- @
-- addRanges FontRanges.DefaultRanges <> addText "Привет"
-- @
newtype RangesBuilderSetup = RangesBuilderSetup
{ applyToBuilder :: GlyphRangesBuilder -> IO ()
}
instance Semigroup RangesBuilderSetup where
RangesBuilderSetup f <> RangesBuilderSetup g =
RangesBuilderSetup \fc -> f fc >> g fc
instance Monoid RangesBuilderSetup where
mempty = RangesBuilderSetup (const mempty)
-- | Rebuild font atlas with provided configuration
-- and return corresponding structure of font handles
-- to be used with 'withFont'.
--
-- Accepts any 'Traversable' instance, so you are free to use
-- lists, maps or custom structures.
rebuild :: (MonadIO m, Traversable t) => t FontSource -> m (t Font)
rebuild sources = liftIO $ Managed.with action pure
where
action = do
clear
fonts <- traverse setupFont sources
build
return fonts
-- | Reset font atlas, clearing internal data
--
-- Alias for 'Raw.clearFontAtlas'
clear :: (MonadIO m) => m ()
clear = Raw.clearFontAtlas
-- | Build font atlas
--
-- Alias for 'Raw.buildFontAtlas'
build :: (MonadIO m) => m ()
build = Raw.buildFontAtlas
-- | Load a font from TTF file.
--
-- Specify font path and atlas glyph size.
--
-- Use 'Raw.addFontDefault' if you want to retain built-in font too.
--
-- Call 'build' after adding all the fonts,
-- particularly if you're loading them from memory or use custom glyphs.
-- Or stick to `rebuild` function.
--
-- Call backend-specific `CreateFontsTexture` before using 'newFrame'.
addFontFromFileTTF :: MonadIO m
=> FilePath -- ^ Font file path
-> Float -- ^ Font size in pixels
-> Maybe FontConfig -- ^ Configuration data
-> Maybe GlyphRanges -- ^ Glyph ranges to use
-> m (Maybe Font) -- ^ Returns font handle, if added successfully
addFontFromFileTTF font size config ranges = liftIO do
res@(Font ptr) <- withCString font \fontPtr ->
Raw.addFontFromFileTTF
fontPtr
(CFloat size)
(fromMaybe (FontConfig nullPtr) config)
(fromMaybe (GlyphRanges nullPtr) ranges)
pure $
if castPtr ptr == nullPtr
then Nothing
else Just res
-- FIXME: turn off asserts, so it would work
addFontFromFileTTF_ :: MonadIO m
=> FilePath -- ^ Font file path
-> Float -- ^ Font size in pixels
-> m (Maybe Raw.Font) -- ^ Returns font handle, if added successfully
addFontFromFileTTF_ font size =
addFontFromFileTTF font size Nothing Nothing
-- | Load a font with provided configuration, return its handle
-- and defer range builder and config destructors, if needed.
setupFont :: (MonadManaged m) => FontSource -> m Font
setupFont = \case
DefaultFont ->
Raw.addFontDefault
FromTTF path size configSetup ranges -> do
glyphRanges' <- setupRanges ranges
config <- managed (withConfig configSetup)
mFont <- addFontFromFileTTF path size config glyphRanges'
case mFont of
Nothing ->
liftIO . fail $ "Couldn't load font from " <> path
Just font ->
pure font
-- | Configure glyph ranges with provided configuration, return a handle
-- and defer builder destructors, if needed.
setupRanges :: (MonadManaged m) => Ranges -> m (Maybe GlyphRanges)
setupRanges = \case
RangesRaw ranges ->
pure $ Just ranges
RangesBuiltin builtin ->
pure $ GlyphRanges.builtinSetup builtin
RangesBuilder settings -> do
built <- managed $ withRanges settings
pure $ Just built
-- | Perform glyph ranges build based on provided configuration,
-- and execute a computation with built glyph ranges.
withRanges :: (MonadUnliftIO m) => RangesBuilderSetup -> (GlyphRanges -> m a) -> m a
withRanges (RangesBuilderSetup setup) fn =
bracket acquire release execute
where
acquire = do
builder <- GlyphRanges.new
liftIO $ setup builder
rangesVec <- GlyphRanges.buildRangesVector builder
return (rangesVec, builder)
release (rangesVec, builder) = do
GlyphRanges.destroyRangesVector rangesVec
GlyphRanges.destroy builder
execute (rangesVec, _) =
fn (GlyphRanges.fromRangesVector rangesVec)
-- | Configure font config with provided setup,
-- and execute a computation with built object.
-- return its handle and list of resource destructors.
withConfig :: (MonadUnliftIO m) => Maybe ConfigSetup -> (Maybe FontConfig -> m a) -> m a
withConfig mSetup action =
case mSetup of
Nothing ->
action Nothing
Just (ConfigSetup setup) ->
bracket acquire (FontConfig.destroy) (action . Just)
where
acquire = do
config <- FontConfig.new
liftIO $ setup config
return config
-- | Single Unicode character
addChar :: ImWchar -> RangesBuilderSetup
addChar char =
RangesBuilderSetup \builder ->
GlyphRanges.addChar builder char
-- | UTF-8 string
addText :: String -> RangesBuilderSetup
addText str =
RangesBuilderSetup \builder ->
withCString str (GlyphRanges.addText builder)
-- | Existing ranges (as is)
addRangesRaw :: GlyphRanges -> RangesBuilderSetup
addRangesRaw ranges =
RangesBuilderSetup \builder ->
GlyphRanges.addRanges builder ranges
-- | Existing ranges (through settings interface)
addRanges :: Ranges -> RangesBuilderSetup
addRanges = \case
RangesRaw ranges ->
addRangesRaw ranges
RangesBuilder settings ->
settings
RangesBuiltin builtin ->
addRangesRaw (GlyphRanges.getBuiltin builtin)
-- | TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself).
--
-- By default, it is @true@
fontDataOwnedByAtlas :: Bool -> ConfigSetup
fontDataOwnedByAtlas value =
ConfigSetup \fc ->
FontConfig.setFontDataOwnedByAtlas fc (bool 0 1 value)
-- | Index of font within TTF/OTF file.
--
-- By default, it is @0@
fontNo :: Int -> ConfigSetup
fontNo value =
ConfigSetup \fc ->
FontConfig.setFontNo fc (fromIntegral value)
-- | Size in pixels for rasterizer
--
-- More or less maps to the resulting font height.
--
-- Implicitly set by @addFont...@ functions.
sizePixels :: Float -> ConfigSetup
sizePixels value =
ConfigSetup \fc ->
FontConfig.setSizePixels fc (CFloat value)
-- | Rasterize at higher quality for sub-pixel positioning.
--
-- Note: the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
-- Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details.
--
-- By default, it is @3@
oversampleH :: Int -> ConfigSetup
oversampleH value =
ConfigSetup \fc ->
FontConfig.setOversampleH fc (fromIntegral value)
-- | Rasterize at higher quality for sub-pixel positioning.
--
-- This is not really useful as we don't use sub-pixel positions on the Y axis.
--
-- By default, it is @1@
oversampleV :: Int -> ConfigSetup
oversampleV value =
ConfigSetup \fc ->
FontConfig.setOversampleV fc (fromIntegral value)
-- | Align every glyph to pixel boundary.
--
-- Useful if you are merging a non-pixel aligned font with the default font.
-- If enabled, you can set OversampleH/V to 1.
--
-- By default, it is @false@
pixelSnapH :: Bool -> ConfigSetup
pixelSnapH value =
ConfigSetup \fc ->
FontConfig.setPixelSnapH fc (bool 0 1 value)
-- | Extra spacing (in pixels) between glyphs.
--
-- Only X axis is supported for now.
--
-- By default, it is @0, 0@
glyphExtraSpacing :: (Float, Float) -> ConfigSetup
glyphExtraSpacing (x, y) =
ConfigSetup \fc ->
Foreign.with (ImVec2 x y) (FontConfig.setGlyphExtraSpacing fc)
-- | Offset all glyphs from this font input.
--
-- By default, it is @0, 0@
glyphOffset :: (Float, Float) -> ConfigSetup
glyphOffset (x, y) =
ConfigSetup \fc ->
Foreign.with (ImVec2 x y) (FontConfig.setGlyphOffset fc)
-- | Pointer to a user-provided list of Unicode range.
--
-- 2 values per range, inclusive. Zero-terminated list.
--
-- THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE.
--
-- By default, it is @NULL@
glyphRanges :: GlyphRanges -> ConfigSetup
glyphRanges value =
ConfigSetup \fc ->
FontConfig.setGlyphRanges fc value
-- | Minimum AdvanceX for glyphs.
--
-- Set Min to align font icons, set both Min/Max to enforce mono-space font.
--
-- By default, it is @0@
glyphMinAdvanceX :: Float -> ConfigSetup
glyphMinAdvanceX value =
ConfigSetup \fc ->
FontConfig.setGlyphMinAdvanceX fc (CFloat value)
-- | Maximum AdvanceX for glyphs.
--
-- By default, it is @FLT_MAX@.
glyphMaxAdvanceX :: Float -> ConfigSetup
glyphMaxAdvanceX value =
ConfigSetup \fc ->
FontConfig.setGlyphMaxAdvanceX fc (CFloat value)
-- | Merge into previous ImFont, so you can combine multiple inputs font into one ImFont.
--
-- e.g. ASCII font + icons + Japanese glyphs.
-- You may want to use @GlyphOffset.y@ when merging font of different heights.
--
-- By default, it is @false@
mergeMode :: Bool -> ConfigSetup
mergeMode value =
ConfigSetup \fc ->
FontConfig.setMergeMode fc (bool 0 1 value)
-- | Settings for custom font GlyphRanges.
--
-- THIS IS BUILDER IMPLEMENTATION DEPENDENT.
--
-- By default, it is @0@. Leave it so if unsure.
fontBuilderFlags :: Int -> ConfigSetup
fontBuilderFlags value =
ConfigSetup \fc ->
FontConfig.setFontBuilderFlags fc (fromIntegral value)
-- | Brighten (>1.0f) or darken (<1.0f) font output.
--
-- Brightening small fonts may be a good workaround to make them more readable.
--
-- By default, it is @1.0f@.
rasterizerMultiply :: Float -> ConfigSetup
rasterizerMultiply value =
ConfigSetup \fc ->
FontConfig.setRasterizerMultiply fc (CFloat value)
-- | Explicitly specify unicode codepoint of ellipsis character.
--
-- When fonts are being merged first specified ellipsis will be used.
--
-- By default, it is @-1@
ellipsisChar :: ImWchar -> ConfigSetup
ellipsisChar value =
ConfigSetup \fc ->
FontConfig.setEllipsisChar fc value