packages feed

nano-ui-0.1.0.0: test/integration/Cases/Cache.hs

module Cases.Cache
  ( runMetricCacheInvalidationTest
  , runWidgetPlacementCacheTest
  , runLayoutPaintStateTest
  ) where

import Control.Monad (forM_, void)
import Control.Exception (evaluate)
import Data.ByteString qualified as BS
import Data.IORef (IORef, readIORef, writeIORef)
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Ptr (castPtr)
import NanoUI
import NanoUI.Context (Context (..))
import NanoUI.Layout.Arena
  ( NodeType (..), addNodeFromLayout, getRect, setNodeText, setNodeValue
  , setStyleIdx, setWidgetId
  )
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, assertGt)
import NanoUI.Testing.Harness (withInputOff)
import System.Mem.StableName (makeStableName)

-- Copy the mutable draw buffers before another frame can reuse them. Counts
-- alone cannot detect stale geometry, colors or translated text.
snapshotDraw :: DrawData -> IO (BS.ByteString, BS.ByteString, [DrawCmd])
snapshotDraw draw = do
  vertices <- withForeignPtr (drawVertices draw) $ \p ->
    BS.packCStringLen (castPtr p, drawVertexCount draw * vertexSize)
  indices <- withForeignPtr (drawIndices draw) $ \p ->
    BS.packCStringLen (castPtr p, drawIndexCount draw * indexSize)
  pure (vertices, indices, drawCmdElems draw)

runMetricCacheInvalidationTest :: Context -> IORef Int -> IO ()
runMetricCacheInvalidationTest ctx failed = do
  let inp = withInputOff 400 300
      width c = do
        void $ runFrame c inp (button "ABC")
        (_, _, w, _) <- getRect (ctxNodeArena c) 0
        pure w
      a = withMeasureText ctx (\_ -> pure (200, 20))
      b = withMeasureText ctx (\_ -> pure (80, 12))
  original <- width ctx
  wa <- width a
  wb <- width b
  assertGt failed wa original
  assertGt failed wa wb
  -- Both variants derive from the same parent and share mutable caches. An
  -- incremented pure Int revision would collide here; returning to A matters.
  assertEq failed wa =<< width a
  assertEq failed original =<< width ctx

  -- Each supported pure modifier must invalidate both layout and placement.
  forM_
    [ (\c -> withFontMetrics c (monospaceMetrics 24), void (button "ABC"))
    , (\c -> withMonoFontMetrics c (monospaceMetrics 24), void (labelWith fontMono "ABC"))
    , (\c -> withFontResolver c (\_ _ _ _ -> pure (monospaceMetrics 24, False))
            (\_ _ _ _ _ -> pure (200, 24)), void (labelWith (fontSize 24) "ABC"))
    ] $ \(configure, ui) -> do
      warm <- newContext
      void $ runFrame warm inp ui
      let configured = configure warm
      (_, _, draw, _) <- runFrame configured inp ui
      actual <- snapshotDraw draw
      spans <- collectTextSpans configured
      fresh <- configure <$> newContext
      (_, _, coldDraw, _) <- runFrame fresh inp ui
      expected <- snapshotDraw coldDraw
      assertEq failed actual expected
      assertEq failed spans =<< collectTextSpans fresh

-- A table header's width and style stay fixed while alignment and its parent
-- origin change independently. This exercises the placement cache's key.
header :: Context -> AlignX -> Float -> Float -> NanoUI ()
header ctx ax x y = uiIO $ do
  let na = ctxNodeArena ctx
  parent <- addNodeFromLayout na NodeContainer (-1) $
    (fixedWH 320 160 defaultLayout) {layoutPadding = Padding x 0 y 0}
  i <- addNodeFromLayout na NodeButton parent $
    (fixedWH 200 30 defaultLayout) {layoutAlignX = ax}
  setWidgetId na i (WidgetId 123)
  setNodeText na i "Header"
  setStyleIdx na i 0x80000000

runWidgetPlacementCacheTest :: Context -> IORef Int -> IO ()
runWidgetPlacementCacheTest _ctx failed =
  forM_ [1, 1.5, 2] $ \scale -> do
    base <- newContext
    let ctx = withFontMetrics base ((monospaceMetrics 12) {fmSnapScale = scale})
        inp = withInputOff 400 300
    void $ runFrame ctx inp (header ctx AlignStart 0 0)
    start <- collectTextSpans ctx
    (_, _, draw, _) <- runFrame ctx inp (header ctx AlignEnd 0 0)
    aligned <- snapshotDraw draw
    end <- collectTextSpans ctx
    assert failed (start /= end)
    freshBase <- newContext
    let fresh = withFontMetrics freshBase ((monospaceMetrics 12) {fmSnapScale = scale})
    (_, _, expectedDraw, _) <- runFrame fresh inp (header fresh AlignEnd 0 0)
    assertEq failed aligned =<< snapshotDraw expectedDraw

    cache <- readIORef (ctxWidgetTextCache ctx) >>= evaluate >>= makeStableName
    forM_ [(0.25, 0.5), (9.75, 3.25), (0, 0)] $ \(x, y) -> do
      (_, _, movedDraw, _) <- runFrame ctx inp (header ctx AlignEnd x y)
      moved <- snapshotDraw movedDraw
      cache' <- readIORef (ctxWidgetTextCache ctx) >>= evaluate >>= makeStableName
      assert failed (cache == cache')
      -- Force a fresh placement at the same origin and compare actual bytes.
      clearMeasureCache fresh
      (_, _, coldDraw, _) <- runFrame fresh inp (header fresh AlignEnd x y)
      assertEq failed moved =<< snapshotDraw coldDraw

runLayoutPaintStateTest :: Context -> IORef Int -> IO ()
runLayoutPaintStateTest ctx failed = do
  let inp = withInputOff 400 300
      ui value color = do
        column $ do
          box (fixedWH 30 30) color
          uiIO $ do
            let na = ctxNodeArena ctx
            i <- addNodeFromLayout na NodeSlider 0 (fixedWH 200 30 defaultLayout)
            setWidgetId na i (WidgetId 123)
            setNodeText na i ""
            setNodeValue na i value
          void (labelWith (fontColor color) "paint only")
      red = colorRGBA 255 0 0 255
      blue = colorRGBA 0 0 255 255
  (_, _, firstDraw, _) <- runFrame ctx inp (ui 0.2 red)
  first <- snapshotDraw firstDraw
  cache <- readIORef (ctxLayoutCache ctx) >>= evaluate >>= makeStableName
  (_, _, changedDraw, _) <- runFrame ctx inp (ui 0.8 blue)
  changed <- snapshotDraw changedDraw
  cache' <- readIORef (ctxLayoutCache ctx) >>= evaluate >>= makeStableName
  assert failed (cache == cache')
  assert failed (first /= changed)
  -- A cache hit must preserve this frame's slider value and paint colors.
  writeIORef (ctxLayoutCache ctx) Nothing
  (_, _, coldDraw, _) <- runFrame ctx inp (ui 0.8 blue)
  assertEq failed changed =<< snapshotDraw coldDraw