packages feed

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

module Cases.Combo
  ( runComboBlurCommitTest
  , runComboEscapeRevertTest
  , runComboFilterTest
  , runComboHoverHighlightTest
  , runComboKeyboardPickTest
  , runComboMousePickTest
  , runComboScrollbarDragTest
  , runComboWheelScrollTest
  ) where

import Data.IORef (IORef, newIORef)
import Data.Text qualified as T
import NanoUI
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, withInput)
import NanoUI.Testing.Harness (clickPair, hasText, held, keyInp, tabInp, warmup2)

comboOpts :: [T.Text]
comboOpts = ["Alpha Sans", "Beta Serif", "Gamma Mono", "Delta Round"]

-- Enough options to overflow the dropdown's visible window (8 rows). Names
-- are zero-padded so no name is a substring of another.
comboLongOpts :: [T.Text]
comboLongOpts =
  [ "Fam " <> (if i < 10 then "0" else "") <> T.pack (show i)
  | i <- [1 .. 12 :: Int]
  ]

-- Typing filters the suggestion list case-insensitively and never selects
-- anything on its own: Enter with no highlight leaves the typed text alone.
runComboFilterTest :: Context -> IORef Int -> IO ()
runComboFilterTest ctx failed = do
  textRef <- newIORef ""
  let inp0 = withInput 320 100
      ui = held textRef (comboBox' "Font" comboOpts)
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx (inp0 {inputChars = "ga"}) ui
  overlays <- collectOverlayTextSpans ctx inp0
  assert failed (hasText "Gamma Mono" overlays)
  assert failed (not (hasText "Alpha Sans" overlays))
  ((_, t), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assertEq failed t "ga"

-- Up/Down move the keyboard highlight (Down from nothing selects the first
-- row), and Enter commits it.
runComboKeyboardPickTest :: Context -> IORef Int -> IO ()
runComboKeyboardPickTest ctx failed = do
  let inp0 = withInput 320 100
      ui = comboBox' "Font" comboOpts ""
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx (keyInp KeyDown inp0) ui
  _ <- runFrame ctx (keyInp KeyDown inp0) ui
  ((r, t), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assert failed (respChanged r)
  assertEq failed t "Beta Serif"
  spans <- collectTextSpans ctx
  assert failed (hasText "Beta Serif" spans)

-- Clicking a suggestion row commits its option text.
runComboMousePickTest :: Context -> IORef Int -> IO ()
runComboMousePickTest ctx failed = do
  let inp0 = withInput 320 200
      ui = comboBox' "Font" comboOpts ""
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx inp0 ui
  overlays <- collectOverlayTextSpans ctx inp0
  case [r | (r, txt, _, _, _) <- overlays, "Beta Serif" `T.isInfixOf` txt] of
    (rowRect : _) -> do
      let cx = rectX rowRect + rectW rowRect / 2
          cy = rectY rowRect + rectH rowRect / 2
          (press, release) = clickPair inp0 (V2 cx cy)
      _ <- runFrame ctx press ui
      ((r, t), _, _, _) <- runFrame ctx release ui
      assert failed (respChanged r)
      assertEq failed t "Beta Serif"
      -- Picking defocuses the field: the dropdown is visible exactly while
      -- focused, so the menu disappears with the pick.
      focus <- getFocusId ctx
      assertEq failed focus (WidgetId 0)
      overlaysClosed <- collectOverlayTextSpans ctx release
      assert failed (not (hasText "Alpha Sans" overlaysClosed))
    _ -> assert failed False

-- Hovering a suggestion row highlights it (hover paint, becomes the Enter
-- target) but never commits by itself; Enter then commits the hovered row.
runComboHoverHighlightTest :: Context -> IORef Int -> IO ()
runComboHoverHighlightTest ctx failed = do
  let inp0 = withInput 320 200
      ui = comboBox' "Font" comboOpts ""
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx inp0 ui
  overlays <- collectOverlayTextSpans ctx inp0
  case [r | (r, txt, _, _, _) <- overlays, "Delta Round" `T.isInfixOf` txt] of
    (rowRect : _) -> do
      let hover =
            inp0
              { inputMousePos = V2 (rectX rowRect + rectW rowRect / 2) (rectY rowRect + rectH rowRect / 2)
              }
      _ <- runFrame ctx hover ui
      -- Hover alone must not commit anything.
      ((r0, t0), _, _, _) <- runFrame ctx hover ui
      assert failed (not (respChanged r0) && T.null t0)
      -- Menu rows show the pointer cursor while hovered.
      ptr <- cursorKindIs ctx hover UiCursorPointer
      assert failed ptr
      -- The hovered row carries the hover background, the others do not.
      overlaysHover <- collectOverlayTextSpans ctx hover
      let bgFor needle = [bg | (_, txt, _, bg, _) <- overlaysHover, needle `T.isInfixOf` txt]
      case (bgFor "Delta Round", bgFor "Alpha Sans") of
        ([dBg], [aBg]) -> assert failed (dBg /= aBg)
        _ -> assert failed False
      -- Enter commits the hovered row.
      ((r1, t1), _, _, _) <- runFrame ctx (keyInp KeyEnter hover) ui
      assert failed (respChanged r1)
      assertEq failed t1 "Delta Round"
    _ -> assert failed False

-- Dragging the vertical scrollbar thumb scrolls the list, and releasing the
-- drag over a row must not commit it.
runComboScrollbarDragTest :: Context -> IORef Int -> IO ()
runComboScrollbarDragTest ctx failed = do
  let inp0 = withInput 320 300
      ui = comboBox' "Fonts" comboLongOpts ""
  (resp, _) <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx inp0 ui
  overlays0 <- collectOverlayTextSpans ctx inp0
  assert failed (hasText "Fam 01" overlays0)
  let Rect rx ry rw rh = respRect resp
      -- Mirrors the overlay geometry: gap 4, item 28, lane 10 wide, rows
      -- flush at the drop rect's top (no outer margin).
      dropY = ry + rh + 4
      trackX = rx + rw - 5
      press = inp0 {inputMousePos = V2 trackX (dropY + 200), inputMouseDown = True, inputMousePressed = True}
  _ <- runFrame ctx press ui
  _ <- runFrame ctx press {inputMousePressed = False} ui
  -- Release over a row position (bottom of the list): must not pick.
  let release = press {inputMouseDown = False, inputMouseReleased = True}
  ((r, t), _, _, _) <- runFrame ctx release ui
  assert failed (T.null t && not (respChanged r))
  overlays1 <- collectOverlayTextSpans ctx inp0
  assert failed (not (hasText "Fam 01" overlays1))
  assert failed (hasText "Fam 12" overlays1)

-- Typing edits the live text without committing, word-wise editing keys
-- (Ctrl+Backspace) work like in the plain text input, and losing focus
-- commits the text.
runComboBlurCommitTest :: Context -> IORef Int -> IO ()
runComboBlurCommitTest ctx failed = do
  textRef <- newIORef ""
  let inp0 = withInput 320 200
      ui = column (held textRef (comboBox' "Font" comboOpts))
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  ((rA, tA), _, _, _) <- runFrame ctx (inp0 {inputChars = "N"}) ui
  assertEq failed tA "N"
  assert failed (not (respChanged rA))
  ((rB, tB), _, _, _) <- runFrame ctx (inp0 {inputChars = "o"}) ui
  assertEq failed tB "No"
  assert failed (not (respChanged rB))
  _ <- runFrame ctx (inp0 {inputChars = " bar"}) ui
  _ <- runFrame ctx (inp0 {inputKeys = inputKeysFromList [KeyBackspace], inputModifiers = Modifiers False True False}) ui
  ((rW, tW), _, _, _) <- runFrame ctx inp0 ui
  assertEq failed tW "No "
  assert failed (not (respChanged rW))
  _ <- runFrame ctx (keyInp KeyBackspace inp0) ui
  -- Click far away: focus clears after the UI pass, and the frame after the
  -- blur commits the typed text.
  let away = inp0 {inputMousePos = V2 310 5, inputMouseDown = True, inputMousePressed = True}
  _ <- runFrame ctx away ui
  ((rC, tC), _, _, _) <- runFrame ctx inp0 {inputMouseReleased = True} ui
  assertEq failed tC "No"
  assert failed (respChanged rC)
  ((rD, _), _, _, _) <- runFrame ctx inp0 ui
  assert failed (not (respChanged rD))

-- Unfocused, the combo is just a search field: the value is visible and no
-- dropdown overlay exists. Escape cancels an edit: the live text reverts to
-- the last committed value without a commit pulse, and the dropdown closes.
runComboEscapeRevertTest :: Context -> IORef Int -> IO ()
runComboEscapeRevertTest ctx failed = do
  textRef <- newIORef "Inter"
  let inp0 = withInput 320 200
      ui = held textRef (comboBox' "Font" comboOpts)
  (r0, t0) <- warmup2 ctx inp0 ui
  assertEq failed t0 "Inter"
  assert failed (not (respChanged r0))
  overlays0 <- collectOverlayTextSpans ctx inp0
  assert failed (not (hasText "Alpha Sans" overlays0))
  spans0 <- collectTextSpans ctx
  assert failed (hasText "Inter" spans0)
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx (inp0 {inputChars = "No"}) ui
  ((r, t), _, _, _) <- runFrame ctx (keyInp KeyEscape inp0) ui
  assert failed (not (respChanged r))
  assertEq failed t "Inter"
  focus <- getFocusId ctx
  assertEq failed focus (WidgetId 0)
  overlays <- collectOverlayTextSpans ctx inp0
  assert failed (not (hasText "Alpha Sans" overlays))

-- The wheel scrolls the suggestion list while the pointer is over the open
-- dropdown: a horizontal wheel shifts rows that overflow the dropdown width
-- (clamped), and a vertical notch slides the window past the first rows.
runComboWheelScrollTest :: Context -> IORef Int -> IO ()
runComboWheelScrollTest ctx failed = do
  let inp0 = withInput 200 260
      long = "A Very Long Font Family Name That Overflows"
      ui = comboBox' "Fonts" (comboLongOpts ++ [long]) ""
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  _ <- runFrame ctx inp0 ui
  overlays0 <- collectOverlayTextSpans ctx inp0
  assert failed (hasText "Fam 01" overlays0)
  assert failed (not (hasText "Fam 09" overlays0))
  case [r | (r, txt, _, _, _) <- overlays0, "Fam 01" `T.isInfixOf` txt] of
    (rowRect : _) -> do
      let overList = inp0 {inputMousePos = V2 (rectX rowRect + 4) (rectY rowRect + rectH rowRect / 2)}
      _ <- runFrame ctx overList ui
      _ <- runFrame ctx overList {inputScroll = V2 5 0} ui
      overlaysX <- collectOverlayTextSpans ctx overList
      case [r | (r, txt, _, _, _) <- overlaysX, "Fam 01" `T.isInfixOf` txt] of
        (after : _) -> assert failed (rectX after < rectX rowRect - 50)
        _ -> assert failed False
      -- The x-shift is clamped: a huge wheel does not push rows out of reach.
      _ <- runFrame ctx overList {inputScroll = V2 1000 0} ui
      overlaysClamped <- collectOverlayTextSpans ctx overList
      assert failed (hasText "Fam 01" overlaysClamped)
      _ <- runFrame ctx overList {inputScroll = V2 0 1} ui
      overlays1 <- collectOverlayTextSpans ctx overList
      -- One wheel notch scrolls three rows past "Fam 01".
      assert failed (not (hasText "Fam 01" overlays1))
      assert failed (hasText "Fam 04" overlays1)
      assert failed (hasText "Fam 11" overlays1)
    _ -> assert failed False