Add FontManager, based on nanovg font management logic

This commit is contained in:
Francisco Vallarino 2021-07-03 19:39:46 -03:00
parent b07cbb5327
commit edcb106656
11 changed files with 526 additions and 9 deletions

170
cbits/fontmanager.c Normal file
View File

@ -0,0 +1,170 @@
// Based on code from memononen's https://github.com/memononen/nanovg
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "fontstash.h"
#include "fontmanager.h"
static float fm__minf(float a, float b) {
return a < b ? a : b;
}
static float fm__maxf(float a, float b) {
return a > b ? a : b;
}
FMcontext* fmInit()
{
FMcontext *ctx;
FONSparams fontParams;
ctx = (FMcontext*) malloc(sizeof(FMcontext));
memset(ctx, 0, sizeof(FMcontext));
memset(&fontParams, 0, sizeof(fontParams));
fontParams.width = INIT_FONTIMAGE_SIZE;
fontParams.height = INIT_FONTIMAGE_SIZE;
fontParams.flags = FONS_ZERO_TOPLEFT;
fontParams.renderCreate = NULL;
fontParams.renderUpdate = NULL;
fontParams.renderDraw = NULL;
fontParams.renderDelete = NULL;
fontParams.userPtr = NULL;
// Initialize font manager context
ctx->fs = fonsCreateInternal(&fontParams);
ctx->fontSize = 16.0f;
ctx->letterSpacing = 0.0f;
ctx->lineHeight = 1.0f;
ctx->fontBlur = 0.0f;
ctx->textAlign = ALIGN_LEFT | ALIGN_BASELINE;
ctx->fontId = 0;
return ctx;
}
int fmCreateFont(FMcontext* ctx, const char* name, const char* filename)
{
return fonsAddFont(ctx->fs, name, filename, 0);
}
void fmFontFace(FMcontext* ctx, const char* font)
{
ctx->fontId = fonsGetFontByName(ctx->fs, font);
}
void fmFontSize(FMcontext* ctx, float size)
{
ctx->fontSize = size;
}
void fmFontBlur(FMcontext* ctx, float blur)
{
ctx->fontBlur = blur;
}
void fmTextLetterSpacing(FMcontext* ctx, float spacing)
{
ctx->letterSpacing = spacing;
}
void fmTextLineHeight(FMcontext* ctx, float lineHeight)
{
ctx->lineHeight = lineHeight;
}
void fmTextMetrics(FMcontext* ctx, float* ascender, float* descender, float* lineh)
{
float scale = 1.0f;
float invscale = 1.0f / scale;
if (ctx->fontId == FONS_INVALID) return;
fonsSetSize(ctx->fs, ctx->fontSize * scale);
fonsSetSpacing(ctx->fs, ctx->letterSpacing * scale);
fonsSetBlur(ctx->fs, ctx->fontBlur * scale);
fonsSetAlign(ctx->fs, ctx->textAlign);
fonsSetFont(ctx->fs, ctx->fontId);
fonsVertMetrics(ctx->fs, ascender, descender, lineh);
if (ascender != NULL)
*ascender *= invscale;
if (descender != NULL)
*descender *= invscale;
if (lineh != NULL)
*lineh *= invscale;
}
float fmTextBounds(FMcontext* ctx, float x, float y, const char* string, const char* end, float* bounds)
{
float scale = 1.0f; //nvg__getFontScale(state) * ctx->devicePxRatio;
float invscale = 1.0f / scale;
float width;
if (ctx->fontId == FONS_INVALID) return 0;
fonsSetSize(ctx->fs, ctx->fontSize*scale);
fonsSetSpacing(ctx->fs, ctx->letterSpacing*scale);
fonsSetBlur(ctx->fs, ctx->fontBlur*scale);
fonsSetAlign(ctx->fs, ctx->textAlign);
fonsSetFont(ctx->fs, ctx->fontId);
width = fonsTextBounds(ctx->fs, x*scale, y*scale, string, end, bounds);
if (bounds != NULL) {
// Use line bounds for height.
fonsLineBounds(ctx->fs, y*scale, &bounds[1], &bounds[3]);
bounds[0] *= invscale;
bounds[1] *= invscale;
bounds[2] *= invscale;
bounds[3] *= invscale;
}
return width * invscale;
}
int fmTextGlyphPositions(FMcontext* ctx, float x, float y, const char* string, const char* end, FMGglyphPosition* positions, int maxPositions)
{
float scale = 1.0f; // nvg__getFontScale(state) * ctx->devicePxRatio;
float invscale = 1.0f / scale;
FONStextIter iter, prevIter;
FONSquad q;
int npos = 0;
if (ctx->fontId == FONS_INVALID) return 0;
if (end == NULL)
end = string + strlen(string);
if (string == end)
return 0;
fonsSetSize(ctx->fs, ctx->fontSize*scale);
fonsSetSpacing(ctx->fs, ctx->letterSpacing*scale);
fonsSetBlur(ctx->fs, ctx->fontBlur*scale);
fonsSetAlign(ctx->fs, ctx->textAlign);
fonsSetFont(ctx->fs, ctx->fontId);
fonsTextIterInit(ctx->fs, &iter, x*scale, y*scale, string, end, FONS_GLYPH_BITMAP_OPTIONAL);
prevIter = iter;
while (fonsTextIterNext(ctx->fs, &iter, &q)) {
// can not retrieve glyph?
if (iter.prevGlyphIndex < 0 && fonsResetAtlas(ctx->fs, MAX_FONTIMAGE_SIZE, MAX_FONTIMAGE_SIZE)) {
iter = prevIter;
fonsTextIterNext(ctx->fs, &iter, &q); // try again
}
prevIter = iter;
positions[npos].str = iter.str;
positions[npos].x = iter.x * invscale;
positions[npos].minx = fm__minf(iter.x, q.x0) * invscale;
positions[npos].maxx = fm__maxf(iter.nextx, q.x1) * invscale;
positions[npos].miny = fm__minf(iter.y, q.y0) * invscale;
positions[npos].maxy = fm__maxf(iter.nexty, q.y1) * invscale;
npos++;
if (npos >= maxPositions)
break;
}
return npos;
}

56
cbits/fontmanager.h Normal file
View File

@ -0,0 +1,56 @@
// Based on code from memononen's https://github.com/memononen/nanovg
#ifndef FONT_MANAGER_H
#define FONT_MANAGER_H
#include <stdio.h>
#include "fontstash.h"
#define INIT_FONTIMAGE_SIZE 512
#define MAX_FONTIMAGE_SIZE 2048
#define MAX_FONTIMAGES 4
#define ALIGN_LEFT 1
#define ALIGN_BASELINE 64
struct FMcontext {
struct FONScontext* fs;
float fontSize;
float letterSpacing;
float lineHeight;
float fontBlur;
int textAlign;
int fontId;
};
typedef struct FMcontext FMcontext;
struct FMGglyphPosition {
const char* str; // Position of the glyph in the input string.
float x; // The x-coordinate of the logical glyph position.
float minx, maxx; // The bounds of the glyph shape.
float miny, maxy; // The vertical bounds of the glyph shape.
};
typedef struct FMGglyphPosition FMGglyphPosition;
FMcontext* fmInit();
int fmCreateFont(FMcontext* ctx, const char* name, const char* filename);
void fmFontFace(FMcontext* ctx, const char* font);
void fmFontSize(FMcontext* ctx, float size);
void fmFontBlur(FMcontext* ctx, float blur);
void fmTextLetterSpacing(FMcontext* ctx, float spacing);
void fmTextLineHeight(FMcontext* ctx, float lineHeight);
void fmTextMetrics(FMcontext* ctx, float* ascender, float* descender, float* lineh);
float fmTextBounds(FMcontext* ctx, float x, float y, const char* string, const char* end, float* bounds);
int fmTextGlyphPositions(FMcontext* ctx, float x, float y, const char* string, const char* end, FMGglyphPosition* positions, int maxPositions);
#endif

View File

@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: e9cd0fb27bcf456724b1c412cf85e4063c1b7d69e6d55a9c36b6260203aeb70d
-- hash: 184b12bd9a4df73b6b30a51ea36e1a3573485f4aebe0034a6a5de5aa1becc26c
name: monomer
version: 0.1.0.0
@ -55,6 +55,8 @@ library
Monomer.Event.Util
Monomer.Graphics
Monomer.Graphics.ColorTable
Monomer.Graphics.FFI
Monomer.Graphics.FontManager
Monomer.Graphics.Lens
Monomer.Graphics.NanoVGRenderer
Monomer.Graphics.Text
@ -136,9 +138,14 @@ library
OverloadedStrings
ghc-options: -fwarn-incomplete-patterns
cc-options: -fPIC
include-dirs:
cbits
install-includes:
fontmanager.h
c-sources:
cbits/glew.c
cbits/dpi.c
cbits/fontmanager.c
cbits/glew.c
build-depends:
JuicyPixels
, OpenGL

View File

@ -53,9 +53,12 @@ dependencies:
library:
source-dirs: src
include-dirs: cbits
install-includes: fontmanager.h
c-sources:
- cbits/glew.c
- cbits/dpi.c
- cbits/fontmanager.c
- cbits/glew.c
cc-options:
- -fPIC
ghc-options:

View File

@ -0,0 +1,176 @@
{-|
Module : Monomer.Graphics.FFI
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Provides functions for getting text dimensions and metrics.
Based on code from cocreature's https://github.com/cocreature/nanovg-hs
-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Monomer.Graphics.FFI where
import Control.Monad (forM)
import Data.ByteString (useAsCString)
import Data.Text (Text)
import Data.Text.Foreign (withCStringLen)
import Data.Sequence (Seq)
import Foreign
import Foreign.C (CString)
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Monomer.Graphics.Types (GlyphPos(..))
#include "fontmanager.h"
-- | Vector of 4 strict elements
data V4 a = V4 !a !a !a !a
deriving (Show, Read, Eq, Ord)
newtype Bounds
= Bounds (V4 CFloat)
deriving (Show, Read, Eq, Ord)
instance Storable Bounds where
sizeOf _ = sizeOf (0 :: CFloat) * 4
alignment _ = alignment (0 :: CFloat)
peek p =
do let p' = castPtr p :: Ptr CFloat
a <- peekElemOff p' 0
b <- peekElemOff p' 1
c <- peekElemOff p' 2
d <- peekElemOff p' 3
pure (Bounds (V4 a b c d))
poke p (Bounds (V4 a b c d)) =
do let p' = castPtr p :: Ptr CFloat
pokeElemOff p' 0 a
pokeElemOff p' 1 b
pokeElemOff p' 2 c
pokeElemOff p' 3 d
data GlyphPosition = GlyphPosition {
-- | Pointer of the glyph in the input string.
str :: !(Ptr CChar),
-- | The x-coordinate of the logical glyph position.
glyphX :: !CFloat,
-- | The left bound of the glyph shape.
glyphPosMinX :: !CFloat,
-- | The right bound of the glyph shape.
glyphPosMaxX :: !CFloat,
-- | The lower bound of the glyph shape.
glyphPosMinY :: !CFloat,
-- | The upper bound of the glyph shape.
glyphPosMaxY :: !CFloat
} deriving (Show, Eq, Ord)
instance Storable GlyphPosition where
sizeOf _ = {# sizeof FMGglyphPosition #}
alignment _ = {#alignof FMGglyphPosition#}
peek p =
do str <- {#get FMGglyphPosition->str#} p
x <- {#get FMGglyphPosition->x#} p
minx <- {#get FMGglyphPosition->minx#} p
maxx <- {#get FMGglyphPosition->maxx#} p
miny <- {#get FMGglyphPosition->miny#} p
maxy <- {#get FMGglyphPosition->maxy#} p
pure (GlyphPosition str x minx maxx miny maxy)
poke p (GlyphPosition str x minx maxx miny maxy) =
do {#set FMGglyphPosition->str#} p str
{#set FMGglyphPosition->x#} p x
{#set FMGglyphPosition->minx#} p minx
{#set FMGglyphPosition->maxx#} p maxx
{#set FMGglyphPosition->miny#} p miny
{#set FMGglyphPosition->maxy#} p maxy
{#pointer *FMGglyphPosition as GlyphPositionPtr -> GlyphPosition#}
peekBounds :: Ptr CFloat -> IO Bounds
peekBounds = peek . castPtr
allocaBounds :: (Ptr CFloat -> IO b) -> IO b
allocaBounds f = alloca (\(p :: Ptr Bounds) -> f (castPtr p))
withCString :: Text -> (CString -> IO b) -> IO b
withCString t = useAsCString (T.encodeUtf8 t)
withText :: Text -> (CString -> IO b) -> IO b
withText t = useAsCString (T.encodeUtf8 t)
-- | Marshalling helper for a constant 'nullPtr'
withNull :: (Ptr a -> b) -> b
withNull f = f nullPtr
-- Common
{# pointer *FMcontext as FMContext newtype #}
deriving instance Storable FMContext
{# fun unsafe fmInit {} -> `FMContext' #}
{# fun unsafe fmCreateFont {`FMContext', withCString*`Text', withCString*`Text'} -> `Int' #}
{# fun unsafe fmFontFace {`FMContext', withCString*`Text'} -> `()' #}
{# fun unsafe fmFontSize {`FMContext', `Double'} -> `()' #}
{# fun unsafe fmFontBlur {`FMContext', `Double'} -> `()' #}
{# fun unsafe fmTextLetterSpacing {`FMContext', `Double'} -> `()' #}
{# fun unsafe fmTextLineHeight {`FMContext', `Double'} -> `()' #}
{# fun unsafe fmTextMetrics as fmTextMetrics_ {`FMContext', alloca- `CFloat' peek*, alloca- `CFloat' peek*, alloca- `CFloat' peek*} -> `()' #}
fmTextMetrics :: FMContext -> IO (Double, Double, Double)
fmTextMetrics fm = do
(asc, desc, lineh) <- fmTextMetrics_ fm
return (realToFrac asc, realToFrac desc, realToFrac lineh)
{# fun unsafe fmTextBounds as fmTextBounds_
{`FMContext', `Double', `Double', withText*`Text', withNull-`Ptr CUChar', allocaBounds-`Bounds'peekBounds*} -> `Double' #}
fmTextBounds :: FMContext -> Double -> Double -> Text -> IO (Double, Double, Double, Double)
fmTextBounds fm x y text = do
(_, Bounds (V4 x1 y1 x2 y2)) <- fmTextBounds_ fm x y text
return (realToFrac x1, realToFrac y1, realToFrac x2, realToFrac y2)
{# fun unsafe fmTextGlyphPositions as fmTextGlyphPositions_
{`FMContext', `Double', `Double', id`Ptr CChar', id`Ptr CChar', `GlyphPositionPtr', `CInt'} -> `CInt' #}
fmTextGlyphPositions :: FMContext -> Double -> Double -> Text -> IO (Seq GlyphPos)
fmTextGlyphPositions c x y text =
withCStringLen text $ \(ptr, len) -> do
let startPtr = ptr
let endPtr = ptr `plusPtr` len
allocaBytesAligned bufferSize align $ \arrayPtr -> do
count <- fmTextGlyphPositions_ c x y startPtr endPtr arrayPtr maxGlyphs
glyphs <- Seq.fromList <$> readChunk arrayPtr count
return $ Seq.zipWith toGlyphPos (Seq.fromList (T.unpack text)) glyphs
where
maxGlyphs = fromIntegral (T.length text)
bufferSize = sizeOf (undefined :: GlyphPosition) * fromIntegral maxGlyphs
align = alignment (undefined :: GlyphPosition)
readChunk :: GlyphPositionPtr -> CInt -> IO [GlyphPosition]
readChunk arrayPtr count = forM [0..count] $ \i ->
peekElemOff arrayPtr (fromIntegral i)
toGlyphPos chr glyph = GlyphPos {
_glpGlyph = chr,
_glpXMin = realToFrac (glyphPosMinX glyph),
_glpXMax = realToFrac (glyphPosMaxX glyph),
_glpYMin = realToFrac (glyphPosMinY glyph),
_glpYMax = realToFrac (glyphPosMaxY glyph),
_glpW = realToFrac (glyphPosMaxX glyph - glyphPosMinX glyph),
_glpH = realToFrac (glyphPosMaxY glyph - glyphPosMinY glyph)
}

View File

@ -0,0 +1,83 @@
{-|
Module : Monomer.Graphics.FontManager
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Provides functions for getting text dimensions and metrics.
-}
{-# LANGUAGE RecordWildCards #-}
module Monomer.Graphics.FontManager (
makeFontManager
) where
import Control.Monad (foldM, when)
import Data.Sequence (Seq)
import Data.Text (Text)
import System.IO.Unsafe
import qualified Control.Concurrent.Lock as LK
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import Monomer.Common.BasicTypes
import Monomer.Graphics.FFI
import Monomer.Graphics.Types
makeFontManager
:: [FontDef] -- ^ The font definitions.
-> Double -- ^ The device pixel rate.
-> IO FontManager -- ^ The created renderer.
makeFontManager fonts dpr = do
ctx <- fmInit
lock <- LK.new
validFonts <- foldM (loadFont ctx) [] fonts
when (null validFonts) $
putStrLn "Could not find any valid fonts. Text will fail to be displayed."
return $ newManager ctx dpr lock
newManager :: FMContext -> Double -> LK.Lock -> FontManager
newManager ctx dpr lock = FontManager {..} where
fcomputeTextMetrics font fontSize = unsafePerformIO $ LK.with lock $ do
setFont ctx dpr font fontSize
(asc, desc, lineh) <- fmTextMetrics ctx
lowerX <- Seq.lookup 0 <$> fmTextGlyphPositions ctx 0 0 "x"
let heightLowerX = case lowerX of
Just lx -> _glpYMax lx - _glpYMin lx
Nothing -> realToFrac asc
return $ TextMetrics {
_txmAsc = asc / dpr,
_txmDesc = desc / dpr,
_txmLineH = lineh / dpr,
_txmLowerX = realToFrac heightLowerX / dpr
}
fcomputeTextSize font fontSize text = unsafePerformIO $ LK.with lock $ do
setFont ctx dpr font fontSize
(x1, y1, x2, y2) <- fmTextBounds ctx 0 0 text
return $ Size (realToFrac (x2 - x1) / dpr) (realToFrac (y2 - y1) / dpr)
fcomputeGlyphsPos font fontSize text = unsafePerformIO $ LK.with lock $ do
setFont ctx dpr font fontSize
fmTextGlyphPositions ctx 0 0 text
loadFont :: FMContext -> [Text] -> FontDef -> IO [Text]
loadFont ctx fonts (FontDef name path) = do
res <- fmCreateFont ctx name path
if res >= 0
then return $ path : fonts
else putStrLn ("Failed to load font: " ++ T.unpack name) >> return fonts
setFont :: FMContext -> Double -> Font -> FontSize -> IO ()
setFont ctx dpr (Font name) (FontSize size) = do
fmFontFace ctx name
fmFontSize ctx $ realToFrac $ size * dpr

View File

@ -323,7 +323,10 @@ newRenderer c dpr lock envRef = Renderer {..} where
_glpGlyph = glyph,
_glpXMin = realToFrac (VG.glyphPosMinX pos) / dpr,
_glpXMax = realToFrac (VG.glyphPosMaxX pos) / dpr,
_glpW = realToFrac (VG.glyphPosMaxX pos - VG.glyphPosMinX pos) / dpr
_glpYMin = realToFrac (VG.glyphPosMinY pos) / dpr,
_glpYMax = realToFrac (VG.glyphPosMaxY pos) / dpr,
_glpW = realToFrac (VG.glyphPosMaxX pos - VG.glyphPosMinX pos) / dpr,
_glpH = realToFrac (VG.glyphPosMaxY pos - VG.glyphPosMinY pos) / dpr
}
renderText !point font fontSize message = do

View File

@ -121,10 +121,13 @@ instance Default AlignTV where
-- | Information of a text glyph instance.
data GlyphPos = GlyphPos {
_glpGlyph :: {-# UNPACK #-} !Char, -- ^ The representer character.
_glpGlyph :: {-# UNPACK #-} !Char, -- ^ The represented character.
_glpXMin :: {-# UNPACK #-} !Double, -- ^ The min x coordinate.
_glpXMax :: {-# UNPACK #-} !Double, -- ^ The max x coordinate.
_glpW :: {-# UNPACK #-} !Double -- ^ The glyph width.
_glpYMin :: {-# UNPACK #-} !Double, -- ^ The min x coordinate.
_glpYMax :: {-# UNPACK #-} !Double, -- ^ The max x coordinate.
_glpW :: {-# UNPACK #-} !Double, -- ^ The glyph width.
_glpH :: {-# UNPACK #-} !Double -- ^ The glyph height.
} deriving (Eq, Show, Generic)
instance Default GlyphPos where
@ -132,7 +135,10 @@ instance Default GlyphPos where
_glpGlyph = ' ',
_glpXMin = 0,
_glpXMax = 0,
_glpW = 0
_glpYMin = 0,
_glpYMax = 0,
_glpW = 0,
_glpH = 0
}
-- | Text flags for single or multiline.
@ -202,6 +208,14 @@ data ImageDef = ImageDef {
_idfFlags :: [ImageFlag] -- ^ The image flags.
} deriving (Eq, Show, Generic)
data FontManager = FontManager {
fcomputeTextMetrics :: Font -> FontSize -> TextMetrics,
-- | Returns the text size of the text given font and size.
fcomputeTextSize :: Font -> FontSize -> Text -> Size,
-- | Returns the glyphs of the text given font and size.
fcomputeGlyphsPos :: Font -> FontSize -> Text -> Seq GlyphPos
}
-- | Low level rendering definitions.
data Renderer = Renderer {
-- | Begins a new frame.

View File

@ -722,7 +722,7 @@ findClosestGlyphPos state point = newPos where
textLen = getGlyphsMax (_ifsGlyphs state)
glyphs
| Seq.null (_ifsGlyphs state) = Seq.empty
| otherwise = _ifsGlyphs state |> GlyphPos ' ' textLen 0 0
| otherwise = _ifsGlyphs state |> GlyphPos ' ' textLen 0 0 0 0 0
glyphStart i g = (i, abs (_glpXMin g - localX))
pairs = Seq.mapWithIndex glyphStart glyphs
cpm (_, g1) (_, g2) = compare g1 g2

View File

@ -827,7 +827,7 @@ findClosestGlyphPos state point = (newPos, lineIdx) where
textLen = getGlyphsMax lineGlyphs
glyphs
| Seq.null lineGlyphs = Seq.empty
| otherwise = lineGlyphs |> GlyphPos ' ' textLen 0 0
| otherwise = lineGlyphs |> GlyphPos ' ' textLen 0 0 0 0 0
glyphStart i g = (i, abs (_glpXMin g - x))
pairs = Seq.mapWithIndex glyphStart glyphs
cpm (_, g1) (_, g2) = compare g1 g2

View File

@ -729,6 +729,11 @@
- Can image be aligned to right when fitting is applied?
Next
- Create custom font handling functions
- Use nanovg's font stash, mostly import logic from nanovg functions
- Separate size calculation from rendering
- Renderer should not be in WidgetEnv
- Revert change to nanovg
- Think about rendering with custom beginFrame for some widgets
- Could work for rounded images.
- Improve base theme creation (scale argument?)