packages feed

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

module Cases.Damage
  ( runDamageBoundsResolutionTest
  , runExplicitDamageWidgetTest
  , runDamageQueueClearedPerFrameTest
  , runStateChangeDamageTest
  , runOrphanAnimationDamageSettlesTest
  , runVersionedDrawingDamageTest
  , runClipFrameBackdropTest
  , runTextAreaSelectAllDamageTest
  ) where

import Data.IORef (IORef, readIORef, writeIORef)
import NanoUI
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, withInput)
import NanoUI.Testing.Harness (centerOf, drawQuads, runClick, tabInp, warmup2, withInputOff)

-- | A new version on a versioned drawing repaints its rect. Paint rebuilds the
-- ops once the version moves, and nothing else damages them, so a clip frame
-- would otherwise keep the pixels it drew last time.
runVersionedDrawingDamageTest :: Context -> IORef Int -> IO ()
runVersionedDrawingDamageTest ctx failed = do
  let inp = withInput 400 300
      ui version = column $ do
        _ <- label "Other"
        drawingVersioned version (fixedWH 80 40) $ \r ->
          runCanvas (drawRect r (colorRGBA 255 0 0 255))
  resp <- warmup2 ctx inp (ui 1)
  _ <- takeDamage ctx
  _ <- runFrame ctx inp (ui 2)
  dmg <- takeDamage ctx
  case dmg of
    DamageClip clip -> assert failed (covers clip (respRect resp))
    DamageFull -> assert failed False

-- | Whether the first rect contains the second.
covers :: Rect -> Rect -> Bool
covers (Rect cx cy cw ch) (Rect x y w h) =
  cx <= x && cy <= y && cx + cw >= x + w && cy + ch >= y + h

runDamageBoundsResolutionTest :: Context -> IORef Int -> IO ()
runDamageBoundsResolutionTest _ failed = do
  let base = Rect 10 20 100 50
      rSelf = resolveDamageRect DamageSelf base
      rInflated = resolveDamageRect (DamageInflated 8.0) base
      rExact = resolveDamageRect (DamageExact (Rect 0 0 500 500)) base
      rCustom = resolveDamageRect (DamageCustom (\(Rect x y w h) -> Rect (x - 1) (y - 2) (w + 10) (h + 20))) base
      rNone = resolveDamageRect DamageNone base

  assertEq failed rSelf base
  assertEq failed rInflated (Rect 2 12 116 66)
  assertEq failed rExact (Rect 0 0 500 500)
  assertEq failed rCustom (Rect 9 18 110 70)
  assertEq failed rNone (Rect 0 0 0 0)
  assertEq failed (resolveDamageRect (DamageUnion (DamageInflated 4.0) (DamageInflated 8.0)) base)
    (rectUnion (Rect 6 16 108 58) (Rect 2 12 116 66))
  -- DamageNone is the identity of a union rather than a rect at the origin.
  assertEq failed (resolveDamageRect (DamageUnion DamageSelf DamageNone) base) base
  assertEq failed (resolveDamageRect (DamageUnion DamageNone (DamageExact base)) (Rect 0 0 0 0)) base

runExplicitDamageWidgetTest :: Context -> IORef Int -> IO ()
runExplicitDamageWidgetTest ctx failed = do
  let inp = withInput 400 300
      ui = columnWith (padAll 20) $ do
        w1 <- button' "First"
        w2 <- button' "Second"
        pure (w1, w2)
  -- Warmup to establish solved layout rects
  _ <- runFrame ctx inp ui
  ((w1, _), _, _, _) <- runFrame ctx inp ui
  _ <- takeDamage ctx

  -- Queue explicit widget damage
  let testUi = columnWith (padAll 20) $ do
        w1' <- button' "First"
        w2' <- button' "Second"
        damageWidgetNow (respId w1') (DamageInflated sliderDamageSlop)
        pure (w1', w2')
  _ <- runFrame ctx inp testUi
  dmg <- takeDamage ctx
  let Rect x1 y1 w1Len h1Len = respRect w1
      expected = rectInflate sliderDamageSlop (Rect x1 y1 w1Len h1Len)
      approxEq (Rect a b c d) (Rect e f g h) =
        abs (a - e) < 0.05 && abs (b - f) < 0.05 && abs (c - g) < 0.05 && abs (d - h) < 0.05
  case dmg of
    DamageFull -> assert failed False
    DamageClip r -> assert failed (approxEq r expected)

runDamageQueueClearedPerFrameTest :: Context -> IORef Int -> IO ()
runDamageQueueClearedPerFrameTest ctx failed = do
  let inp = withInput 400 300
      ui = column (label "Static content")
  _ <- warmup2 ctx inp ui
  _ <- takeDamage ctx

  -- Explicit damage in this frame
  let damagedUi = column $ do
        damageRectNow (Rect 5 5 20 20)
        label "Static content"
  _ <- runFrame ctx inp damagedUi
  dmg1 <- takeDamage ctx
  case dmg1 of
    DamageClip r -> assertEq failed r (Rect 5 5 20 20)
    _ -> assert failed False

  -- Next frame without damage requests: damage is empty
  _ <- runFrame ctx inp ui
  dmg2 <- takeDamage ctx
  assert failed (damageIsEmpty dmg2)

  -- Explicit full-window damage, again only for its own frame
  let fullDamagedUi = column $ do
        damageFullNow
        label "Static content"
  _ <- runFrame ctx inp fullDamagedUi
  dmg3 <- takeDamage ctx
  assertEq failed dmg3 DamageFull
  _ <- runFrame ctx inp ui
  dmg4 <- takeDamage ctx
  assert failed (damageIsEmpty dmg4)

runStateChangeDamageTest :: Context -> IORef Int -> IO ()
runStateChangeDamageTest ctx failed = do
  let inp0 = withInput 400 300
      ui = do
        (name, setName) <- useText ""
        row $ do
          label ("Left pane: " <> name)
          setName =<< textInput name

  -- Warm up and focus textInput via Tab
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- takeDamage ctx

  -- Type a character into focused textInput
  _ <- runFrame ctx (inp0 {inputChars = "a"}) ui
  dmg <- takeDamage ctx
  assertEq failed dmg DamageFull

runOrphanAnimationDamageSettlesTest :: Context -> IORef Int -> IO ()
runOrphanAnimationDamageSettlesTest ctx failed = do
  let winInp = withInput 400 300
      inp = winInp {inputDeltaTime = 0.05}
      withBar = columnWith (padAll 20) $ do
        bar <- currentId
        spacer (Fixed 40) (Fixed 20)
        pure bar
      withoutBar = columnWith (padAll 20) (pure ())
  -- Warm up: the bar widget occupies a nonzero 40x20 rect in the arena.
  (wid, _, _, _) <- runFrame ctx inp withBar
  _ <- takeDamage ctx
  -- keepAnimating-style perpetual animation on an established widget.
  startAnimation ctx wid 0 1 1e9
  -- Widget present and animating => damage is a clip over it, not a
  -- whole-window repaint.
  _ <- runFrame ctx inp withBar
  dmgAnimated <- takeDamage ctx
  case dmgAnimated of
    DamageFull -> assert failed False
    DamageClip r -> assert failed (rectW r > 0 && rectH r > 0)
  -- Widget leaves the arena (tab switch). The first absent frame may repaint
  -- its old region.
  _ <- runFrame ctx inp withoutBar
  _ <- takeDamage ctx
  -- The perpetual animation is still live, but it must not force the whole
  -- window to repaint forever after its widget is gone.
  _ <- runFrame ctx inp withoutBar
  live <- anyAnimating ctx
  assert failed live
  dmgAbsent <- takeDamage ctx
  assert failed (damageIsEmpty dmgAbsent)
  -- Guard: a freshly started animation on a widget that has never been laid
  -- out still escalates to a full repaint for its first rect-less frame.
  ctx2 <- newContext
  startAnimation ctx2 (WidgetId 777) 0 1 0.3
  _ <- runFrame ctx2 winInp (label "bare")
  dmgFresh <- takeDamage ctx2
  assertEq failed dmgFresh DamageFull
  _ <- runFrame ctx2 winInp (label "bare")
  dmgFresh2 <- takeDamage ctx2
  assert failed (dmgFresh2 /= DamageFull)

-- | A clip frame repaints its region from the window backdrop, as a full frame
-- repaints from the cleared window. An idle menu-bar title has no fill, so
-- without the backdrop the hover highlight it just lost would stay in the
-- retain texture.
runClipFrameBackdropTest :: Context -> IORef Int -> IO ()
runClipFrameBackdropTest ctx failed = do
  writeIORef (ctxPaintFull ctx) False
  let inp0 = withInputOff 400 300
      ui = rowWith (tight . fillW . fixedH 28) $ do
        file <- menuButton' "File" False
        _ <- menuButton' "Edit" False
        pure file
  file <- warmup2 ctx inp0 ui
  _ <- runFrame ctx inp0 {inputMousePos = centerOf file} ui
  (_, _, draw, _) <- runFrame ctx inp0 ui
  dmg <- takeDamage ctx
  theme <- readIORef (ctxTheme ctx)
  quads <- drawQuads draw
  case dmg of
    DamageClip clip -> do
      assert failed (covers clip (respRect file))
      case quads of
        (r, c) : _ -> do
          assert failed (covers r clip)
          assertEq failed c (themeWindow theme)
        [] -> assert failed False
    DamageFull -> assert failed False

-- | Ctrl+A repaints the text area on the frame that selects, rather than
-- leaving the highlight to a follow-up frame.
runTextAreaSelectAllDamageTest :: Context -> IORef Int -> IO ()
runTextAreaSelectAllDamageTest ctx failed = do
  -- Frame time lets the hover fade from the click finish; a live fade would
  -- damage the area anyway.
  let inp0 = (withInputOff 800 600) {inputDeltaTime = 0.5}
      ui = column $ do
        _ <- label "Notes"
        fst <$> textAreaWith' (fixedWH 200 80) "hello world"
  area <- warmup2 ctx inp0 ui
  _ <- runClick ctx inp0 ui (centerOf area)
  _ <- warmup2 ctx inp0 ui
  _ <- takeDamage ctx
  _ <- runFrame ctx inp0 {inputChars = "a", inputModifiers = Modifiers False True False} ui
  dmg <- takeDamage ctx
  case dmg of
    DamageClip clip -> assert failed (covers clip (respRect area))
    DamageFull -> assert failed False