packages feed

nano-ui-0.1.0.0: lib/NanoUI/Frame/Focus.hs

{-# LANGUAGE DataKinds #-}

-- | Focus traversal and modal focus constraints.
module NanoUI.Frame.Focus
  ( filterModalFocusables
  , constrainFocusToModal
  , syncWidgetLabels
  , tabNext
  , tabNextFocusables
  ) where

import Control.Monad (filterM, unless, when)
import Data.IORef (readIORef, writeIORef)
import Data.Primitive.PrimArray (readPrimArray)
import qualified Data.IntMap.Strict as IM
import NanoUI.Context (Context (..), WidgetStore (..), getStore, intBool, intKey)
import NanoUI.Frame.Hit (widgetIdInSubtree)
import NanoUI.Id (WidgetId (..), hashWidgetId)
import NanoUI.Layout.Arena
  ( NodeType (NodeCheckbox, NodeRadio, NodeTree)
  , forNodes_
  , getNodeType
  , getParent
  , getStyleIdx
  , getWidgetId
  , setNodeValue
  , topModalNode
  )
import NanoUI.WidgetText (treeDecodeStyle)

tabNext :: WidgetId -> [WidgetId] -> Bool -> WidgetId
tabNext cur ids shift =
  case ids of
    [] -> WidgetId 0
    first : rest ->
      let lastId !prev [] = prev
          lastId _ (x : xs) = lastId x xs
          search _ [] = first
          search prev (x : xs)
            | x == cur = if shift then prev else case xs of
                next : _ -> next
                [] -> first
            | otherwise = search x xs
       in if cur == first && shift
            then lastId first rest
            else search first ids

-- | Scan the live focus buffer. Skip zero ids. No freeze or list copy.
tabNextFocusables :: Context -> WidgetId -> Bool -> IO WidgetId
tabNextFocusables ctx cur shift = do
  n <- readIORef (ctxFocusablesCount ctx)
  arr <- readIORef (ctxFocusables ctx)
  let at i = readPrimArray arr i
      findCur !i
        | i >= n = pure Nothing
        | otherwise = do
            w <- at i
            if w == cur && hashWidgetId w /= 0 then pure (Just i) else findCur (i + 1)
      firstLive !i
        | i >= n = pure (WidgetId 0)
        | otherwise = do
            w <- at i
            if hashWidgetId w /= 0 then pure w else firstLive (i + 1)
      step !i !left
        | left <= 0 = firstLive 0
        | otherwise = do
            let j = if shift then (i - 1 + n) `mod` n else (i + 1) `mod` n
            w <- at j
            if hashWidgetId w /= 0 then pure w else step j (left - 1)
  if n <= 0
    then pure (WidgetId 0)
    else do
      found <- findCur 0
      case found of
        Nothing -> firstLive 0
        Just i -> step i n

filterModalFocusables :: Context -> [WidgetId] -> IO [WidgetId]
filterModalFocusables ctx ids = do
  -- Searching the arena once per focusable makes a large modal's Tab traversal
  -- quadratic. Resolve its root once, then test ancestry for each widget.
  top <- topModalNode (ctxNodeArena ctx)
  case top of
    Nothing -> pure ids
    Just modal -> filterM (widgetIdInSubtree ctx modal) ids

constrainFocusToModal :: Context -> IO ()
constrainFocusToModal ctx = do
  top <- topModalNode (ctxNodeArena ctx)
  case top of
    Nothing -> pure ()
    Just modal -> do
      focus <- readIORef (ctxFocusId ctx)
      when (hashWidgetId focus /= 0) $ do
        ok <- widgetIdInSubtree ctx modal focus
        unless ok $ writeIORef (ctxFocusId ctx) (WidgetId 0)

syncWidgetLabels :: Context -> IO ()
syncWidgetLabels ctx = do
  store <- getStore ctx
  let na = ctxNodeArena ctx
  forNodes_ na $ \idx -> do
    nt <- getNodeType na idx
    wid <- getWidgetId na idx
    let key = intKey wid
    case nt of
      NodeCheckbox ->
        -- Only sync when the widget owns stored state; otherwise keep the
        -- value set from the initial argument during the UI pass.
        case IM.lookup key (storeInt store) of
          Just v -> setNodeValue na idx (if intBool v then 1 else 0)
          Nothing -> pure ()
      _
        -- A radio's option index is its style; a tree row packs its node
        -- index there. Either is selected when its group's stored value names it.
        | nt == NodeRadio || nt == NodeTree -> do
            parent <- getParent na idx
            si <- getStyleIdx na idx
            groupWid <- getWidgetId na parent
            let own
                  | nt == NodeTree, (nodeIdx, _, _, _) <- treeDecodeStyle si = nodeIdx
                  | otherwise = si
                selected = IM.findWithDefault own (intKey groupWid) (storeInt store)
            setNodeValue na idx (if selected == own then 1 else 0)
      _ -> pure ()