packages feed

nano-ui-sdl-0.1.0.0: test/FontEffects.hs

module Main (main) where

import Control.Exception (IOException, evaluate, try)
import Control.Monad (forM_, unless, void)
import Data.List (isInfixOf)
import Data.IORef (writeIORef)
import qualified Data.Text as T
import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
import NanoUI
import NanoUI.Testing (newPixelContext, textIndexAtX)
import NanoUI.Backend.Sdl (NanoUIFont (..), SdlEnv (..), syncDisplay, withSdlBench)
import NanoUI.Context (ctxResolveFont, ctxResolveMeasure)
import System.Environment (setEnv)
import System.Mem (performGC)

main :: IO ()
main = do
  setEnv "SDL_VIDEODRIVER" "dummy"
  setEnv "SDL_RENDER_DRIVER" "software"
  ctx0 <- newPixelContext
  (font, snapshot, width, quad) <- withSdlBench ctx0 $ \ctx env -> do
    (fm, _) <- ctxResolveFont ctx 16 WeightNormal FontStyleNormal FontRegular
    let text = "AV To fi café λ"
    prepared <- prepareFontMetrics fm text
    (hostWidth, _) <- ctxResolveMeasure ctx 16 WeightNormal FontStyleNormal FontRegular text
    measured <- evaluate (lineWidth prepared text)
    unless (abs (hostWidth - measured) < 0.01) $
      fail ("prepared shaped width differs from SDL measurement: " ++ show (hostWidth, measured))
    scaled <- lineWidthIO (scaleFontMetrics 1.5 fm) text
    unless (abs (scaled - measured * 1.5) < 0.01) $
      fail "effectful font scaling lost its metric scale"
    before <- drawShaped fm text
    unless (maybe False (\(ShapedGlyphs q) -> sizeofPrimArray q >= 8 * 10) before) $
      fail "shaped drawing returned no glyph quads"
    -- Exceed both cache caps through measurement only. This must not fill or
    -- reset the atlas, nor alter an already-rasterised run.
    forM_ [1 .. 1100 :: Int] $ \n -> do
      let labelText = "counter " <> T.pack (show n)
      p <- prepareFontMetrics fm labelText
      void (evaluate (lineWidth p labelText))
    after <- drawShaped fm text
    unless (before == after) $
      fail "metric preparation mutated the atlas or raster cache"
    let oversized = T.replicate 1000 "W"
    large <- prepareFontMetrics fm oversized
    largeGlyphs <- drawShaped fm oversized
    unless (maybe False (\(ShapedGlyphs q) -> sizeofPrimArray q == 8 * 1000) largeGlyphs && lineWidth large oversized > 0) $
      fail "text wider than the atlas lost its glyphs"
    shapingChecks fm
    performGC
    again <- lineWidthIO fm text
    unless (abs (again - measured) < 0.01) $
      fail "font metrics changed after cache eviction and GC"
    -- Exercise the actual font/atlas replacement path. Old pure snapshots
    -- remain valid while old native callbacks must reject their closed font.
    writeIORef (sdlFontRequestRef env) (FontSearch [])
    (replacement, _) <- syncDisplay ctx env emptyInput
    expectClosed (drawShaped fm text)
    (fresh, _) <- ctxResolveFont replacement 16 WeightNormal FontStyleNormal FontRegular
    freshWidth <- lineWidthIO fresh text
    freshQuad <- drawShaped fresh text
    unless (abs (freshWidth - measured) < 0.01 && freshQuad /= Nothing) $
      fail "font/atlas replacement failed to restore shaped text"
    pure (fresh, prepared, measured, freshQuad)
  -- Pure metric and quad values are safe to evaluate after the native font,
  -- atlas and SDL session are closed; native effects fail before dereferencing.
  afterClose <- evaluate (lineWidth snapshot "AV To fi café λ")
  unless (afterClose == width) $ fail "immutable snapshot changed after shutdown"
  void (evaluate quad)
  expectClosed (prepareFontMetrics font "new text")
  expectClosed (drawShaped font "AV To fi café λ")
  expectClosed (drawGlyph font 'A')
  putStrLn "font effects: ok"

-- | Shaping reorders right-to-left text and gives every character a caret:
-- an Arabic word's carets run right to left, a mixed line keeps its Latin
-- carets increasing, and a click lands on the nearest caret.
shapingChecks :: FontMetrics -> IO ()
shapingChecks fm = do
  let arabic = "مرحبا"
      mixed = "Hi مرحبا 12"
  pArabic <- prepareFontMetrics fm arabic
  case fmShape pArabic arabic of
    Nothing -> fail "Arabic text was not shaped"
    Just st -> do
      let carets = [indexPrimArray (stCarets st) i | i <- [0 .. sizeofPrimArray (stCarets st) - 1]]
      unless (length carets == T.length arabic + 1) $ fail "Arabic carets do not cover every character"
      -- The script sets the direction, so the word runs right to left
      -- whichever font draws it.
      unless (and (zipWith (>=) carets (drop 1 carets))) $
        fail ("Arabic carets do not decrease: " <> show carets)
      unless (textIndexAtX pArabic arabic (maximum carets + 1) == 0) $
        fail "a click at the right edge of Arabic text did not land before its first character"
  pMixed <- prepareFontMetrics fm mixed
  case fmShape pMixed mixed of
    Nothing -> fail "mixed text was not shaped"
    Just st -> do
      let caret i = indexPrimArray (stCarets st) i
      unless (caret 0 < caret 1 && caret 1 < caret 2) $ fail "Latin carets in a mixed line do not increase"
      unless (caret 9 < caret 10 && caret 10 < caret 11) $ fail "digits after right-to-left text do not run left to right"

expectClosed :: IO a -> IO ()
expectClosed action = do
  result <- try (void action) :: IO (Either IOException ())
  case result of
    Left err | "used after" `isInfixOf` show err -> pure ()
    _ -> fail "retained font callback did not reject its closed native handle"