packages feed

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

-- | A click belongs to the widget its press went down on. Dragging off a
-- widget and letting go over a neighbour must fire nothing.
module Cases.PointerRelease
  ( runReleaseElsewhereTest
  , runRightReleaseElsewhereTest
  , runReleaseReturnsTest
  , runOverlapPressTest
  ) where

import Control.Monad (void)
import Data.IORef (IORef, newIORef)
import Data.Maybe (isJust, isNothing)
import NanoUI
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, withInput)
import NanoUI.Testing.Harness (centerOf, held, warmup2)

-- | Press one widget, drag onto another, release: neither one fires, this
-- frame or the next.
runReleaseElsewhereTest :: Context -> IORef Int -> IO ()
runReleaseElsewhereTest ctx failed = do
  -- The checkbox drives its own state across frames, so a stray toggle sticks
  -- and the value assertions below have something to catch.
  flag <- newIORef False
  let inp0 = withInput 320 240
      ui = column $ do
        a <- button' "Alpha"
        b <- button' "Beta"
        (cb, on) <- held flag (checkbox' "Flag")
        pure (a, b, cb, on)

  (a, b, cb, _) <- warmup2 ctx inp0 ui
  let at r = inp0 {inputMousePos = centerOf r}
      pressOn r = (at r) {inputMouseDown = True, inputMousePressed = True}
      dragTo r p = p {inputMousePos = centerOf r, inputMousePressed = False}
      releaseOn r p = (dragTo r p) {inputMouseDown = False, inputMouseReleased = True}

  -- Button to button.
  _ <- runFrame ctx (pressOn a) ui
  ((_, bDrag, _, _), _, _, _) <- runFrame ctx (dragTo b (pressOn a)) ui
  assert failed (not (respHovered bDrag) && not (respPressed bDrag))
  ((aUp, bUp, _, _), _, _, _) <- runFrame ctx (releaseOn b (pressOn a)) ui
  assert failed (not (respClicked aUp) && not (respClicked bUp))
  ((aNext, bNext, _, _), _, _, _) <- runFrame ctx (at b) ui
  assert failed (not (respClicked aNext) && not (respClicked bNext))

  -- Button to checkbox: the checkbox must not toggle.
  _ <- runFrame ctx (pressOn a) ui
  ((_, _, cbUp, _), _, _, _) <- runFrame ctx (releaseOn cb (pressOn a)) ui
  assert failed (not (respClicked cbUp))
  ((_, _, _, checked), _, _, _) <- runFrame ctx (at cb) ui
  assert failed (not checked)

  -- Checkbox to button: neither fires, and the box stays clear.
  _ <- runFrame ctx (pressOn cb) ui
  ((_, bOver, cbOff, _), _, _, _) <- runFrame ctx (releaseOn b (pressOn cb)) ui
  assert failed (not (respClicked bOver) && not (respClicked cbOff))
  ((_, _, _, stillOff), _, _, _) <- runFrame ctx (at b) ui
  assert failed (not stillOff)

-- | The same rule for the right button: a context menu opens where the right
-- press went down, not where it came up. Also covers the container path, since
-- the menu area's response comes from a container node rather than a leaf.
runRightReleaseElsewhereTest :: Context -> IORef Int -> IO ()
runRightReleaseElsewhereTest ctx failed = do
  let inp0 = withInput 320 240
      ui = column $ do
        a <- button' "Alpha"
        (lbl, menu) <-
          contextMenuArea (fixedH 60 . fillW) (label' "Area") (const (menuItem "Cut"))
        pure (a, lbl, menu)
  (a, lbl, _) <- warmup2 ctx inp0 ui
  let rightPressOn r =
        inp0
          { inputMousePos = centerOf r
          , inputMouseRightDown = True
          , inputMouseRightPressed = True
          }
      rightReleaseOn r p =
        p
          { inputMousePos = centerOf r
          , inputMouseRightPressed = False
          , inputMouseRightDown = False
          , inputMouseRightReleased = True
          }

  -- Right press on the button, release over the menu area: no menu.
  _ <- runFrame ctx (rightPressOn a) ui
  ((aUp, _, menuUp), _, _, _) <- runFrame ctx (rightReleaseOn lbl (rightPressOn a)) ui
  assert failed (not (respRightClicked aUp))
  assert failed (isNothing menuUp)

  -- Right press and release inside the area: the menu opens.
  _ <- runFrame ctx (rightPressOn lbl) ui
  ((_, _, menuSame), _, _, _) <- runFrame ctx (rightReleaseOn lbl (rightPressOn lbl)) ui
  assert failed (isJust menuSame)

-- | Leaving a widget mid-press and coming back still clicks it, and a plain
-- press-release on one widget is unaffected.
runReleaseReturnsTest :: Context -> IORef Int -> IO ()
runReleaseReturnsTest ctx failed = do
  let inp0 = withInput 320 240
      ui = column $ do
        a <- button' "Alpha"
        b <- button' "Beta"
        pure (a, b)
  (a, b) <- warmup2 ctx inp0 ui
  let at r = inp0 {inputMousePos = centerOf r}
      pressOn r = (at r) {inputMouseDown = True, inputMousePressed = True}
      moveTo r p = p {inputMousePos = centerOf r, inputMousePressed = False}
      releaseOn r p = (moveTo r p) {inputMouseDown = False, inputMouseReleased = True}

  -- Straight click.
  _ <- runFrame ctx (pressOn a) ui
  ((aUp, _), _, _, _) <- runFrame ctx (releaseOn a (pressOn a)) ui
  assert failed (respClicked aUp)

  -- Wander off and back before letting go.
  _ <- runFrame ctx (pressOn b) ui
  _ <- runFrame ctx (moveTo a (pressOn b)) ui
  ((_, bBack), _, _, _) <- runFrame ctx (releaseOn b (pressOn b)) ui
  assert failed (respClicked bBack)
  void (runFrame ctx inp0 ui)

-- | Where two widgets overlap, a held press belongs to the one hover lights
-- up: the earlier sibling, which paints on top.
runOverlapPressTest :: Context -> IORef Int -> IO ()
runOverlapPressTest ctx failed = do
  let inp0 = withInput 320 240
      ui = rowWith (gap (-30)) $ do
        a <- buttonWith' (fixedW 80) "Alpha"
        b <- buttonWith' (fixedW 80) "Beta"
        pure (a, b)
  (a, _) <- warmup2 ctx inp0 ui
  let Rect ax ay aw ah = respRect a
      overlap = inp0 {inputMousePos = V2 (ax + aw - 10) (ay + ah / 2)}
      press = overlap {inputMouseDown = True, inputMousePressed = True}
      held' = overlap {inputMouseDown = True}
  _ <- runFrame ctx overlap ui
  hot <- getHotId ctx
  assert failed (hot == respId a)
  _ <- runFrame ctx press ui
  ((aHeld, bHeld), _, _, _) <- runFrame ctx held' ui
  assert failed (respPressed aHeld && not (respPressed bHeld))
  void (runFrame ctx (overlap {inputMouseReleased = True}) ui)
  void (runFrame ctx inp0 ui)