nano-ui-0.1.0.0: test/integration/Cases/Select.hs
module Cases.Select
( runSelectDragToSelectTest
, runSelectKeyboardTest
, runSelectOverlayDamageTest
, runSelectChangeOnceTest
, runSelectCloseKeepsFocusTest
, runSliderCursorTest
, runTreeKeyboardTest
, runTreeSelectTest
) where
import Data.IORef (IORef, newIORef)
import Data.Text qualified as T
import NanoUI
import Data.Primitive.SmallArray qualified as SA
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, withInput)
import NanoUI.Testing.Harness
( assertSpansHas
, centerOf
, clickPair
, hasText
, held
, keyInp
, runClick
, tabInp
, warmup2
)
runSliderCursorTest :: Context -> IORef Int -> IO ()
runSliderCursorTest ctx failed = do
let inp0 = withInput 300 80
ui = column (slider' 0 100 50)
(resp, _) <- warmup2 ctx inp0 ui
let Rect rx ry rw rh = respRect resp
track = sliderTrackBounds rx ry rw rh
trackMid = V2 (rectX track + rectW track / 2) (rectY track + rectH track / 2)
offPos = V2 (rx + rw + 20) (ry + rh + 20)
hoverTrack = inp0 {inputMousePos = trackMid}
_ <- runFrame ctx hoverTrack ui
hoverKind <- uiCursorKind ctx hoverTrack
assertEq failed hoverKind UiCursorGrab
let pressTrack = hoverTrack {inputMouseDown = True, inputMousePressed = True}
_ <- runFrame ctx pressTrack ui
grabbing <- cursorKindIs ctx pressTrack UiCursorGrabbing
assert failed grabbing
let dragOff = pressTrack {inputMousePos = offPos}
_ <- runFrame ctx dragOff ui
grabbingOff <- cursorKindIs ctx dragOff UiCursorGrabbing
assert failed grabbingOff
let hoverOff = inp0 {inputMousePos = offPos}
_ <- runFrame ctx hoverOff ui
isDefault <- cursorKindIs ctx hoverOff UiCursorDefault
assert failed isDefault
runSelectOverlayDamageTest :: Context -> IORef Int -> IO ()
runSelectOverlayDamageTest ctx failed = do
let ui = column (select' ["Low", "Medium", "High"] 0)
inp0 = (withInput 320 160) {inputMousePos = V2 20 20}
(resp, _) <- warmup2 ctx inp0 ui
let pos = centerOf resp
open = snd (clickPair inp0 pos)
_ <- runClick ctx inp0 ui pos
let idle = open {inputMouseReleased = False, inputDeltaTime = 1}
_ <- runFrame ctx idle ui
overlays <- collectOverlayTextSpans ctx idle
case [rectY r | (r, txt, _, _, _) <- overlays, "High" `T.isInfixOf` txt] of
(highY : _) -> do
let overMenu = idle {inputMousePos = V2 (v2X pos) (highY + 0.5)}
need <- needsRedraw ctx idle overMenu
assert failed need
_ <- runFrame ctx overMenu ui
dmg <- takeDamage ctx
assertEq failed dmg DamageFull
[] -> assert failed False
runTreeSelectTest :: Context -> IORef Int -> IO ()
runTreeSelectTest ctx failed = do
let inp0 = withInput 40 12
items = [TreeItem "alpha" [], TreeItem "beta" []]
ui = column (tree' "t" items 0)
(resp, sel0) <- warmup2 ctx inp0 ui
assertEq failed sel0 0
let Rect rx ry _rh rh = respRect resp
(press, release) = clickPair inp0 (V2 (rx + 1) (ry + rh * 0.75))
_ <- runFrame ctx press ui
((_, sel), _, _, _) <- runFrame ctx release ui
assertEq failed sel 1
-- A tree renders expanded, moves its selection with the arrow keys, and
-- Enter collapses the selected parent.
runTreeKeyboardTest :: Context -> IORef Int -> IO ()
runTreeKeyboardTest ctx failed = do
selectedRef <- newIORef 0
let items = SA.smallArrayFromList [TreeItem "root" [TreeItem "child" []], TreeItem "leaf" []]
ui = column (held selectedRef (tree' "k" items))
inp0 = withInput 40 12
_ <- warmup2 ctx inp0 ui
spans0 <- collectTextSpans ctx
assert failed (hasText "root" spans0 && hasText "child" spans0 && hasText "leaf" spans0)
_ <- runFrame ctx (tabInp inp0) ui
((_, sel1), _, _, _) <- runFrame ctx (keyInp KeyDown inp0) ui
assertEq failed sel1 1
((_, sel0), _, _, _) <- runFrame ctx (keyInp KeyUp inp0) ui
assertEq failed sel0 0
_ <- runFrame ctx (keyInp KeyDown inp0) ui
((_, parentSel), _, _, _) <- runFrame ctx (keyInp KeyLeft inp0) ui
assertEq failed parentSel 0
_ <- runFrame ctx (keyInp KeyEnter inp0) ui
_ <- runFrame ctx inp0 ui
spans <- collectTextSpans ctx
assert failed (not (hasText "child" spans))
((_, afterCollapsed), _, _, _) <- runFrame ctx (keyInp KeyDown inp0) ui
assertEq failed afterCollapsed 2
-- Open dropdown rows show the pointer cursor on hover and press, and
-- respChanged fires on the frame the selection changes and not on later
-- frames (regression: it compared the index against the initial one, so it
-- stayed set, and Emit.select emitted, every frame after a pick).
runSelectChangeOnceTest :: Context -> IORef Int -> IO ()
runSelectChangeOnceTest ctx failed = do
indexRef <- newIORef 1
let inp0 = withInput 320 200
ui = held indexRef (select' ["Low", "Medium", "High"])
(resp, _) <- warmup2 ctx inp0 ui
let (openPress, openRelease) = clickPair inp0 (centerOf resp)
_ <- runFrame ctx openPress ui
_ <- runFrame ctx openRelease ui
overlays <- collectOverlayTextSpans ctx openRelease
case [rectY r | (r, txt, _, _, _) <- overlays, "Low" `T.isInfixOf` txt] of
(lowY : _) -> do
let lowPos = V2 (v2X (centerOf resp)) (lowY + 0.5)
hover = inp0 {inputMousePos = lowPos}
(pickPress, pickRelease) = clickPair inp0 lowPos
frame inp = (\((r, i), _, _, _) -> (respChanged r, i)) <$> runFrame ctx inp ui
_ <- runFrame ctx hover ui
hoverKind <- uiCursorKind ctx hover
assertEq failed hoverKind UiCursorPointer
pressed <- frame pickPress
pressKind <- uiCursorKind ctx pickPress
assertEq failed pressKind UiCursorPointer
rest <- mapM frame [pickRelease, inp0, inp0, inp0]
let results = pressed : rest
assertEq failed (map snd rest) [0, 0, 0, 0]
assertEq failed (length (filter fst results)) 1
assertEq failed (map fst (drop 1 rest)) [False, False, False]
[] -> assert failed False
runSelectDragToSelectTest :: Context -> IORef Int -> IO ()
runSelectDragToSelectTest ctx failed = do
let inp0 = withInput 320 200
ui = select' ["Low", "Medium", "High"] 1
(resp, idx0) <- warmup2 ctx inp0 ui
assertEq failed idx0 1
let Rect sx sy sw _ = respRect resp
btnMid = V2 (sx + sw / 2) (sy + 10)
press = inp0 {inputMousePos = btnMid, inputMouseDown = True, inputMousePressed = True}
-- 1. On mousedown, the menu should show up immediately
_ <- runFrame ctx press ui
overlaysPress <- collectOverlayTextSpans ctx press
assert failed (any (\(_, txt, _, _, _) -> "Low" `T.isInfixOf` txt) overlaysPress)
assert failed (any (\(_, txt, _, _, _) -> "High" `T.isInfixOf` txt) overlaysPress)
case [rectY r | (r, txt, _, _, _) <- overlaysPress, "Low" `T.isInfixOf` txt] of
(lowY : _) -> do
-- 2. Move mouse over an item while still pressed
let drag = inp0 {inputMousePos = V2 (sx + sw / 2) (lowY + 0.5), inputMouseDown = True}
_ <- runFrame ctx drag ui
overlaysDrag <- collectOverlayTextSpans ctx drag
assert failed (any (\(_, txt, _, _, _) -> "Low" `T.isInfixOf` txt) overlaysDrag)
kind <- uiCursorKind ctx drag
assertEq failed kind UiCursorPointer
-- 3. Mouseup over the item selects it and closes the menu
let release = drag {inputMouseDown = False, inputMouseReleased = True}
((_, idx1), _, _, _) <- runFrame ctx release ui
assertEq failed idx1 0
overlaysClosed <- collectOverlayTextSpans ctx release
assert failed (not (any (\(_, txt, _, _, _) -> "Low" `T.isInfixOf` txt) overlaysClosed))
spans <- collectTextSpans ctx
assertSpansHas failed "Low" spans
_ -> assert failed False
runSelectKeyboardTest :: Context -> IORef Int -> IO ()
runSelectKeyboardTest ctx failed = do
indexRef <- newIORef 1
let inp0 = withInput 320 200
ui = column (held indexRef (select' (SA.smallArrayFromList ["Low", "Medium", "High"])))
(resp, idx0) <- warmup2 ctx inp0 ui
assertEq failed idx0 1
let (openPress, openRelease) = clickPair inp0 (centerOf resp)
_ <- runFrame ctx openPress ui
_ <- runFrame ctx openRelease ui
_ <- runFrame ctx (keyInp KeyDown openRelease) ui
((_, idx1), _, _, _) <- runFrame ctx openRelease ui
assertEq failed idx1 2
_ <- runFrame ctx (keyInp KeyUp openRelease) ui
((_, idx2), _, _, _) <- runFrame ctx openRelease ui
assertEq failed idx2 1
_ <- runFrame ctx (openRelease {inputKeys = inputKeysFromList [KeyEscape], inputMouseReleased = False}) ui
let idleAfterOpen = openRelease {inputMouseReleased = False}
_ <- runFrame ctx idleAfterOpen ui
overlays <- collectOverlayTextSpans ctx idleAfterOpen
assert failed (not (any (\(_, txt, _, _, _) -> txt `elem` ["Low", "Medium", "High"]) overlays))
_ <- runFrame ctx (tabInp inp0) ui
focus <- getFocusId ctx
assert failed (focus /= WidgetId 0)
_ <- runFrame ctx (keyInp KeyRight inp0) ui
((_, idx3), _, _, _) <- runFrame ctx inp0 ui
assertEq failed idx3 2
closedOverlays <- collectOverlayTextSpans ctx inp0
assert failed (not (any (\(_, txt, _, _, _) -> txt `elem` ["Low", "Medium", "High"]) closedOverlays))
_ <- runFrame ctx (keyInp KeyLeft inp0) ui
((_, idx4), _, _, _) <- runFrame ctx inp0 ui
assertEq failed idx4 1
-- Clicking an open select's own field closes the dropdown and keeps the
-- select focused (regression: the closing press cleared focus).
runSelectCloseKeepsFocusTest :: Context -> IORef Int -> IO ()
runSelectCloseKeepsFocusTest ctx failed = do
indexRef <- newIORef 1
let inp0 = withInput 320 200
ui = column (held indexRef (select' (SA.smallArrayFromList ["Low", "Medium", "High"])))
listed spans = any (\(_, txt, _, _, _) -> txt == "Low") spans
(resp, _) <- warmup2 ctx inp0 ui
let mid = centerOf resp
openRelease = snd (clickPair inp0 mid)
closeRelease = snd (clickPair openRelease mid)
_ <- runClick ctx inp0 ui mid
assert failed . listed =<< collectOverlayTextSpans ctx openRelease
_ <- runClick ctx openRelease ui mid
let idle = closeRelease {inputMouseReleased = False}
_ <- runFrame ctx idle ui
assert failed . not . listed =<< collectOverlayTextSpans ctx idle
focus <- getFocusId ctx
assertEq failed focus (respId resp)