nano-ui-0.1.0.0: test/integration/Cases/Window.hs
module Cases.Window
( runFitHeaderNoShrinkTest
, runOverlayClickThroughTest
, runOverlayPanelLiveTest
, runOverlaySiblingStateTest
, runSeparatorSpanTest
, runWindowCloseDamageTest
, runWindowDragTest
, runWindowOverlayTest
, runWindowResizeHaloHitTest
, runWindowResizeTest
, runWindowScrollGutterTest
, runPageWindowScrollTest
, runWindowScrollOnlyDamageTest
, runWindowContentChurnTest
, runScrolledDebugToggleTest
, runHeadingMonoTruncateTest
) where
import Control.Monad (forM_, replicateM, void, when)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.IntMap.Strict qualified as IM
import Data.Text qualified as T
import NanoUI
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, assertGt, assertLt, withInput)
import NanoUI.Testing.Harness
( assertWheelTitlePinned
, centerOf
, clickPair
, dragWindowEdge
, keyInp
, runClick
, runDragFrom
, spanYOf
, warmup2
, windowTitleGrab
, withInputOff
)
runWindowScrollGutterTest :: Context -> IORef Int -> IO ()
runWindowScrollGutterTest ctx failed = do
let inp0 = withInput 640 360
long = T.pack (replicate 48 'M')
ui = window True "GutterWin" $ do
wide <- labelWith' fillW "WWWW"
kv "Key" long
mapM_ (\i -> label (T.pack ("line " <> show (i :: Int)))) [1 .. 24]
pure wide
(win, mwide) <- warmup2 ctx inp0 ui
let Rect wx _ ww _ = respRect win
-- The body's content keeps the window's side padding before the bar.
contentRight = wx + ww - padR windowPad - scrollBarGutter ScrollBarWindow 0
spans <- collectOverlayTextSpans ctx inp0
let titleYs = [rectY r | (r, txt, _, _, _) <- spans, "GutterWin" `T.isInfixOf` txt]
assert failed (not (null titleYs))
case mwide of
Nothing -> assert failed False
Just wide -> do
let Rect cx _ cw _ = respRect wide
assert failed (cx + cw >= contentRight - 0.5 && cx + cw <= contentRight + 0.01)
runWindowCloseDamageTest :: Context -> IORef Int -> IO ()
runWindowCloseDamageTest ctx failed = do
let ui open = void (window open "Debug" (label "Body"))
inp0 = withInput 640 400
_ <- warmup2 ctx inp0 (ui True)
_ <- runFrame ctx inp0 (ui False)
dmg <- takeDamage ctx
assertEq failed dmg DamageFull
need <- needsRedraw ctx inp0 (inp0 {inputDeltaTime = 1})
assert failed need
runOverlayPanelLiveTest :: Context -> IORef Int -> IO ()
runOverlayPanelLiveTest _ failed = do
let inp = withInputOff 320 240
checkStatic ui = do
ctx <- newContext
_ <- warmup2 ctx inp ui
need <- needsRedraw ctx inp inp
assert failed (not need)
_ <- runFrame ctx inp ui
dmg <- takeDamage ctx
assert failed (damageIsEmpty dmg)
checkDirtyWake ui = do
ctx <- newContext
_ <- runFrame ctx inp ui
markDirty ctx
need <- needsRedraw ctx inp inp
assert failed need
_ <- runFrame ctx inp ui
dmg <- takeDamage ctx
assertEq failed dmg DamageFull
checkStatic (void (window True "Debug" (label "fps 0")))
checkStatic (void (modal True "About" (label "body")))
checkDirtyWake (void (modal True "About" (label "body")))
-- Opening a modal or window must not shift the ids, and so the stored state,
-- of the widgets after it.
runOverlaySiblingStateTest :: Context -> IORef Int -> IO ()
runOverlaySiblingStateTest _ failed = do
let inp = withInput 640 400
ui overlay open edit = do
_ <- overlay open "About" (label "body")
(txt, setTxt) <- useText "start"
when edit (setTxt "edited")
textInput txt
forM_ [modal, window] $ \overlay -> do
ctx <- newContext
_ <- runFrame ctx inp (ui overlay False True)
closed <- warmup2 ctx inp (ui overlay False False)
assertEq failed closed "edited"
opened <- warmup2 ctx inp (ui overlay True False)
assertEq failed opened "edited"
runFitHeaderNoShrinkTest :: Context -> IORef Int -> IO ()
runFitHeaderNoShrinkTest ctx failed = do
let header = panelWith (padXY 16 12 . fillW) (label' "nano-ui SDL3 demo")
only = columnWith (padAll 12 . grow) header
withBody = columnWith (padAll 12 . gap 8 . grow) $ do
h <- header
scrollWith (tight . grow) $
columnWith fillW (mapM_ (label . T.pack . show) [1 .. 40 :: Int])
pure h
tall = withInput 400 800
short = withInput 400 200
_ <- runFrame ctx tall only
(r0, _, _, _) <- runFrame ctx tall only
_ <- runFrame ctx short withBody
(r1, _, _, _) <- runFrame ctx short withBody
assert failed (rectH (respRect r1) + 0.5 >= rectH (respRect r0))
runWindowOverlayTest :: Context -> IORef Int -> IO ()
runWindowOverlayTest ctx failed = do
let inp0 = withInput 640 400
ui = do
outside <- button' "Outside"
(win, mBody) <- window True "Debug" (label "Body")
pure (outside, win, mBody)
closedUi = do
_ <- button "Outside"
(win, mBody) <- window False "Debug" (label "Body")
pure (win, mBody)
do
((win, mBody), _, _, _) <- runFrame ctx inp0 closedUi
assert failed (not (respClicked win))
assert failed (case mBody of Nothing -> True; _ -> False)
closedSpans <- collectOverlayTextSpans ctx inp0
assert failed (not (any (\(_, txt, _, _, _) -> "Debug" `T.isInfixOf` txt) closedSpans))
(outside0, win0, mBody0) <- warmup2 ctx inp0 ui
panels <- floatingPanelRects ctx
overlays <- collectOverlayTextSpans ctx inp0
assert failed (any (\(_, txt, _, _, _) -> "Debug" `T.isInfixOf` txt) overlays)
assert failed (any (\(_, txt, _, _, _) -> "Body" `T.isInfixOf` txt) overlays)
assert failed (not (any (\(_, txt, _, _, _) -> T.strip txt == "X") overlays))
let Rect wx wy ww wh = respRect win0
assert failed (ww >= 100 && wh >= 20)
assert failed (case mBody0 of Just _ -> True; _ -> False)
let (pressOut, releaseOut) = clickPair inp0 (V2 (rectX (respRect outside0) + 8) (rectY (respRect outside0) + 8))
_ <- runFrame ctx pressOut ui
((outsideHit, _, _), _, _, _) <- runFrame ctx releaseOut ui
assert failed (respClicked outsideHit)
let (clickWin, _) = clickPair inp0 (V2 (wx + ww / 2) (wy + wh * 0.7))
((outsideMid, _, _), _, _, _) <- runFrame ctx clickWin ui
assert failed (not (respClicked outsideMid))
let esc = keyInp KeyEscape inp0
((_, winEsc, _), _, _, _) <- runFrame ctx esc ui
assert failed (not (respClicked winEsc))
let Rect px py pw _ =
case map snd (IM.toList panels) of
(r : _) -> r
_ -> respRect win0
closeAt = V2 (px + pw - padR windowPad - 12.5) (py + padT windowPad + 19.5)
(clickClose, releaseClose) = clickPair inp0 closeAt
_ <- runFrame ctx clickClose ui
((_, winClose, _), _, _, _) <- runFrame ctx releaseClose ui
assert failed (respClicked winClose)
runOverlayClickThroughTest :: Context -> IORef Int -> IO ()
runOverlayClickThroughTest ctx failed = do
let
inp0 = withInput 300 220
windowUi = do
outsides <- column (replicateM 10 (button' "Outside"))
(win, mInside) <-
window True "Cover" $ do
button' "Inside"
pure (outsides, win, mInside)
modalUi = do
outsides <- column (replicateM 10 (button' "Outside"))
(dlg, mInside) <-
modal True "Cover" $ do
button' "Inside"
pure (outsides, dlg, mInside)
stackedUi = do
(lo, mLo) <- window True "Low" (button' "LowBtn")
(hi, mHi) <- window True "High" (button' "HighBtn")
pure (lo, mLo, hi, mHi)
childSafePoint cover childRects =
let
Rect x y w h = cover
titleSkip = 40
cands =
[ V2 (x + 6) (y + h * 0.72)
, V2 (x + w - 6) (y + h * 0.72)
, V2 (x + w / 2) (y + h - 6)
, V2 (x + 6) (y + h - 6)
, V2 (x + w - 6) (y + titleSkip + 6)
]
inCover p = rectContains cover p
missesKids p = not (any (`rectContains` p) childRects)
in
case filter (\p -> inCover p && missesKids p) cands of
(p : _) -> Just p
[] -> Nothing
clickNone clicked u pos = do
let (press, release) = clickPair inp0 pos
_ <- runFrame ctx press u
runFrame ctx release u >>= \(hit, _, _, _) -> assert failed (not (clicked hit))
runCovered u = do
_ <- warmup2 ctx inp0 u
((_, cover0, mInside0), _, _, _) <- runFrame ctx inp0 u
let coverRect = respRect cover0
assert failed (rectW coverRect > 0 && rectH coverRect > 0)
case mInside0 of
Nothing -> assert failed False
Just inside0 -> do
let kids = [respRect inside0]
case childSafePoint coverRect kids of
Nothing -> assert failed False
Just pos -> do
let (press, release) = clickPair inp0 pos
_ <- runFrame ctx press u
((outsidesHit, _, _), _, _, _) <- runFrame ctx release u
assert failed (not (any respClicked outsidesHit))
let ir = respRect inside0
ip = V2 (rectX ir + rectW ir / 2) (rectY ir + rectH ir / 2)
assert failed (rectW ir > 0 && rectH ir > 0)
let (ipress, irelease) = clickPair inp0 ip
_ <- runFrame ctx ipress u
((_, _, mInsideHit), _, _, _) <- runFrame ctx irelease u
assert failed (maybe False respClicked mInsideHit)
runStacked = do
_ <- warmup2 ctx inp0 stackedUi
((_, mLo0, hi0, mHi0), _, _, _) <- runFrame ctx inp0 stackedUi
case (mLo0, mHi0) of
(Just loBtn, Just hiBtn) -> do
let cover = respRect hi0
kids = [respRect loBtn, respRect hiBtn]
assert failed (rectW cover > 0 && rectH cover > 0)
case childSafePoint cover kids of
Nothing -> assert failed False
Just pos -> clickNone (\(_, loHit, _, _) -> maybe False respClicked loHit) stackedUi pos
let hp = V2 (rectX (respRect hiBtn) + rectW (respRect hiBtn) / 2) (rectY (respRect hiBtn) + rectH (respRect hiBtn) / 2)
(hpress, hrelease) = clickPair inp0 hp
_ <- runFrame ctx hpress stackedUi
((_, _, _, mHiHit), _, _, _) <- runFrame ctx hrelease stackedUi
assert failed (maybe False respClicked mHiHit)
_ -> assert failed False
runCovered windowUi
runCovered modalUi
runStacked
runWindowDragTest :: Context -> IORef Int -> IO ()
runWindowDragTest ctx failed = do
let inp0 = withInput 640 400
ui = fmap fst (window True "Debug" (label "Body"))
win0 <- warmup2 ctx inp0 ui
let r0 = respRect win0
x0 = rectX r0
y0 = rectY r0
dest = V2 (x0 + 24 - 50) (y0 + 22 + 30)
runDragFrom ctx inp0 ui (windowTitleGrab r0) dest
dmg <- takeDamage ctx
assertEq failed dmg DamageFull
(win1, _, _, _) <- runFrame ctx (inp0 {inputMousePos = dest}) ui
let Rect x1 y1 _ _ = respRect win1
assert failed (x1 < x0 - 10)
assert failed (y1 > y0 + 10)
-- Wheeling over a window's body scrolls the window, not the page, whether the
-- window is declared inside a page scroll area or beside one.
runPageWindowScrollTest :: Context -> IORef Int -> IO ()
runPageWindowScrollTest _ failed = do
let line1 = T.pack "line 1"
title = T.pack "Debug"
debugWindow =
fmap fst $
window True "Debug" $
column $
mapM_ (\i -> label (T.pack ("line " <> show (i :: Int)))) [1 .. 30]
nested = do
(_, win) <- scrollArea (tight . grow) $ do
void (button "OK")
debugWindow
pure win
sibling = do
scrollWith (tight . grow) $ void (label "page")
debugWindow
forM_ [(withInput 320 220, nested), (withInput 640 400, sibling)] $ \(inp0, ui) -> do
ctx <- newContext
win <- warmup2 ctx inp0 ui
let Rect wx _ ww _ = respRect win
spans0 <- collectOverlayTextSpans ctx inp0
case spanYOf line1 spans0 of
[] -> assert failed False
b0 : _ -> do
let wheelAt = V2 (wx + ww / 2) (b0 + 2)
assertWheelTitlePinned failed ctx inp0 ui title line1 wheelAt Nothing
runWindowScrollOnlyDamageTest :: Context -> IORef Int -> IO ()
runWindowScrollOnlyDamageTest ctx failed = do
let inp0 = withInput 640 400
ui =
fmap fst $
window True "Debug" $
column $
mapM_ (\i -> label (T.pack ("line " <> show (i :: Int)))) [1 .. 30]
win <- warmup2 ctx inp0 ui
let wheel =
inp0
{ inputMousePos = centerOf win
, inputScroll = V2 0 1
}
_ <- runFrame ctx wheel ui
dmg <- takeDamage ctx
case dmg of
DamageClip r ->
assert failed (maybe False (\i -> rectW i > 0 && rectH i > 0) (rectIntersect r (respRect win)))
_ -> assert failed False
runWindowContentChurnTest :: Context -> IORef Int -> IO ()
runWindowContentChurnTest ctx failed = do
let inp0 = withInput 640 400
ui k = do
_ <- button "Outside"
fst <$> window True "Debug" (columnWith (tight . gap 4 . minW 300 . fillW) $ do
void $ label (T.pack (replicate (1 + (k `mod` 9)) 'M'))
void $ label "static row"
)
_ <- warmup2 ctx inp0 (ui 0)
counter <- newIORef (1 :: Int)
allClip <- replicateM 30 $ do
k <- readIORef counter
writeIORef counter (k + 1)
_ <- runFrame ctx inp0 (ui k)
dmg <- takeDamage ctx
case dmg of
DamageClip _ -> pure True
_ -> pure False
assert failed (and allClip)
runScrolledDebugToggleTest :: Context -> IORef Int -> IO ()
runScrolledDebugToggleTest ctx failed = do
let inp0 = withInput 640 400
title = T.pack "Debug"
ui = do
(open, setOpen) <- useFlag False
(_, dbgBtn) <- scrollArea (tight . grow) $ do
b <- button' "Debug"
when (respClicked b) (setOpen (not open))
pure b
when open $ void (window True "Debug" (label "fps"))
pure dbgBtn
dbgBtn <- warmup2 ctx inp0 ui
let pos = centerOf dbgBtn
_ <- runClick ctx inp0 ui pos
spans <- collectOverlayTextSpans ctx inp0
let titles = [t | (_, t, _, _, _) <- spans, title `T.isInfixOf` t]
assert failed (not (null titles))
_ <- runFrame ctx inp0 ui
spansAfter <- collectOverlayTextSpans ctx inp0
let titlesAfter = [t | (_, t, _, _, _) <- spansAfter, title `T.isInfixOf` t]
assert failed (not (null titlesAfter))
runWindowResizeTest :: Context -> IORef Int -> IO ()
runWindowResizeTest ctx failed = do
let inp0 = withInput 640 400
ui = fmap fst (window True "Resize" (label "Body"))
_ <- runFrame ctx inp0 ui
(win0, _, _, _) <- runFrame ctx inp0 ui
mrect0 <- getPrevRect ctx (respId win0)
case mrect0 of
Nothing -> assert failed False
Just (Rect x0 y0 w0 h0) -> do
assert failed (w0 > 0 && h0 > 0)
let hoverAt p = inp0 {inputMousePos = p}
expectCursor p kind = do
k <- uiCursorKind ctx (hoverAt p)
assertEq failed k kind
expectCursor (V2 (x0 + w0 + 4) (y0 + h0 + 4)) UiCursorNwseResize
expectCursor (V2 (x0 - 4) (y0 - 4)) UiCursorNwseResize
expectCursor (V2 (x0 + w0 + 4) (y0 - 4)) UiCursorNeswResize
expectCursor (V2 (x0 - 4) (y0 + h0 + 4)) UiCursorNeswResize
expectCursor (V2 (x0 + w0 / 2) (y0 - 4)) UiCursorNsResize
expectCursor (V2 (x0 + w0 / 2) (y0 + h0 + 4)) UiCursorNsResize
expectCursor (V2 (x0 - 4) (y0 + h0 / 2)) UiCursorEwResize
expectCursor (V2 (x0 + w0 + 4) (y0 + h0 / 2)) UiCursorEwResize
expectCursor (V2 (x0 + w0 - 5) (y0 + h0 / 2)) UiCursorEwResize
insideKind <- uiCursorKind ctx (hoverAt (V2 (x0 + w0 - padR windowPad - 4) (y0 + h0 / 2)))
assert failed (insideKind /= UiCursorEwResize)
mSe <- dragWindowEdge ctx inp0 ui (V2 (x0 + w0 + 4) (y0 + h0 + 4)) (V2 (x0 + w0 + 40) (y0 + h0 + 30))
case mSe of
Nothing -> assert failed False
Just (Rect x1 y1 w1 h1) -> do
assertGt failed w1 (w0 + 20)
assertGt failed h1 (h0 + 15)
mW <- dragWindowEdge ctx inp0 ui (V2 (x1 - 4) (y1 + h1 / 2)) (V2 (x1 - 36) (y1 + h1 / 2))
case mW of
Nothing -> assert failed False
Just (Rect xw yw ww hw) -> do
assertGt failed ww (w1 + 15)
assertLt failed xw (x1 - 10)
mN <- dragWindowEdge ctx inp0 ui (V2 (xw + ww / 2) (yw - 4)) (V2 (xw + ww / 2) (yw - 20))
case mN of
Nothing -> assert failed False
Just (Rect xn yn wn hn) -> do
assertGt failed hn (hw + 8)
assertLt failed yn (yw - 5)
let minTitleH = 39 + padT windowPad + padB windowPad
mShort <- dragWindowEdge ctx inp0 ui (V2 (xn + wn / 2) (yn + hn + 4)) (V2 (xn + wn / 2) (yn + 4))
case mShort of
Nothing -> assert failed False
Just (Rect _ _ _ hMin) -> assert failed (hMin + 0.01 >= minTitleH)
runWindowResizeHaloHitTest :: Context -> IORef Int -> IO ()
runWindowResizeHaloHitTest ctx failed = do
let inp0 = withInput 640 400
ui = do
btn <- button' "Hit"
(win, _) <- window True "Resize" (label "Body")
pure (btn, win)
(btn0, win0) <- warmup2 ctx inp0 ui
let Rect bx by bw bh = respRect btn0
Rect x0 y0 _ _ = respRect win0
grab = V2 (x0 + 24) (y0 + 22)
destX = bx + bw + 4
press = inp0 {inputMousePos = grab, inputMouseDown = True, inputMousePressed = True}
_ <- runFrame ctx press ui
let moved = press {inputMousePos = V2 (destX + 24) (y0 + 22), inputMousePressed = False}
_ <- runFrame ctx moved ui
((_, win1), _, _, _) <- runFrame ctx (inp0 {inputMousePos = V2 destX (y0 + 22)}) ui
let Rect x1 y1 _ h1 = respRect win1
hit = V2 (bx + bw - 2) (by + bh - 2)
inHalo = let s = 12
in (v2X hit < x1 && v2X hit >= x1 - s)
&& v2Y hit >= y1 - s
&& v2Y hit <= y1 + h1 + s
isResize k = k == UiCursorEwResize || k == UiCursorNsResize || k == UiCursorNwseResize || k == UiCursorNeswResize
kind <- uiCursorKind ctx (inp0 {inputMousePos = hit})
assert failed (abs (x1 - destX) <= 8)
assert failed inHalo
assert failed (not (isResize kind))
runSeparatorSpanTest :: Context -> IORef Int -> IO ()
runSeparatorSpanTest ctx failed = do
let inp = withInput 200 120
ui = columnWith fillW $ do
label "A"
sid <- currentId
separator
label "B"
pure sid
_ <- runFrame ctx inp ui
(sid, _, _, _) <- runFrame ctx inp ui
mRect <- getPrevRect ctx sid
case mRect of
Just (Rect _ _ w h) -> do
assert failed (w >= 100)
assert failed (h <= 2)
Nothing -> assert failed False
runHeadingMonoTruncateTest :: Context -> IORef Int -> IO ()
runHeadingMonoTruncateTest ctx failed = do
let inp = withInput 1600 600
longPath = T.pack "C:\\Users\\zach\\AppData\\Local\\Microsoft\\Windows\\Fonts\\JetBrainsMono-Regular.ttf"
ui = fst <$> window True "Debug" (do
heading "Draw"
_ <- label "NormalLabel"
kvMono "font" longPath)
win <- warmup2 ctx inp ui
let Rect wx wy ww wh = respRect win
contentRight = wx + ww - padR windowPad
spans <- collectOverlayTextSpans ctx inp
let fontSpans = [(r, t) | (r, t, _, _, _) <- spans, "JetBrainsMono" `T.isInfixOf` t || "..." `T.isInfixOf` t]
case fontSpans of
[(Rect fx _ fw _, t)] -> do
assert failed ("..." `T.isSuffixOf` t)
assert failed (abs (fx + fw - contentRight) < 2.0)
_ -> assert failed False
mWide <- dragWindowEdge ctx inp ui (V2 (wx - 4) (wy + wh / 2)) (V2 (wx - 1100) (wy + wh / 2))
case mWide of
Nothing -> assert failed False
Just (Rect wxWide _ wwWide _) -> do
assertGt failed wwWide (ww + 800)
spansWide <- collectOverlayTextSpans ctx inp
let fontSpansWide = [(r, t) | (r, t, _, _, _) <- spansWide, "JetBrainsMono" `T.isInfixOf` t || "..." `T.isInfixOf` t]
case fontSpansWide of
[(Rect fx2 _ fw2 _, t2)] -> do
assert failed (t2 == longPath)
assert failed (not ("..." `T.isSuffixOf` t2))
let contentRightWide = wxWide + wwWide - padR windowPad
assert failed (abs (fx2 + fw2 - contentRightWide) < 2.0)
_ -> assert failed False