packages feed

nano-ui-0.1.0.0: lib/NanoUI/Hooks.hs

-- | Local state hooks. The store representation varies by value type, but
-- identity, equality checks, and invalidation follow one policy.
module NanoUI.Hooks
  ( useState
  , useFlag
  , useInt
  , useFloat
  , useEnum
  , useText
  , useToggle
  )
where

import Control.Monad (when)
import Data.Dynamic (fromDynamic, toDyn)
import Data.IntMap.Strict qualified as IM
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Effectful (Eff, type (:>))
import NanoUI.Context (getStore, intKey, setStore)
import NanoUI.Monad (Ui, askContext, nextId, uiIO)
import NanoUI.Store (WidgetStore (..), boolInt, bumpMirror, intBool)

useState :: (Typeable a, Eq a, Ui :> es) => a -> Eff es (a, a -> Eff es ())
useState =
  useStored
    (\key store -> IM.lookup key (storeDyn store) >>= fromDynamic)
    ( \key value store -> store {storeDyn = IM.insert key (toDyn value) (storeDyn store)}
    )

useStored ::
  (Eq a, Ui :> es) =>
  (Int -> WidgetStore -> Maybe a)
  -> (Int -> a -> WidgetStore -> WidgetStore)
  -> a
  -> Eff es (a, a -> Eff es ())
useStored lookupValue update initial = do
  wid <- nextId
  ctx <- askContext
  let
    key = intKey wid
    valueIn = fromMaybe initial . lookupValue key
    setValue value = uiIO $ do
      -- A setter can run more than once in a frame. Compare with the latest
      -- store, not the value captured when the hook was evaluated.
      store <- getStore ctx
      when (valueIn store /= value) $
        setStore ctx (bumpMirror (update key value store))
  value <- valueIn <$> uiIO (getStore ctx)
  pure (value, setValue)

useFlag :: Ui :> es => Bool -> Eff es (Bool, Bool -> Eff es ())
useFlag initial = do
  (value, setValue) <- useInt (boolInt initial)
  pure (intBool value, setValue . boolInt)

useInt :: Ui :> es => Int -> Eff es (Int, Int -> Eff es ())
useInt =
  useStored
    (\key -> IM.lookup key . storeInt)
    (\key value store -> store {storeInt = IM.insert key value (storeInt store)})

useFloat :: Ui :> es => Float -> Eff es (Float, Float -> Eff es ())
useFloat =
  useStored
    (\key -> IM.lookup key . storeFloat)
    (\key value store -> store {storeFloat = IM.insert key value (storeFloat store)})

useEnum :: (Enum a, Ui :> es) => a -> Eff es (a, a -> Eff es ())
useEnum initial = do
  (index, setIndex) <- useInt (fromEnum initial)
  pure (toEnum index, setIndex . fromEnum)

useText :: Ui :> es => Text -> Eff es (Text, Text -> Eff es ())
useText =
  useStored
    (\key -> IM.lookup key . storeText)
    (\key value store -> store {storeText = IM.insert key value (storeText store)})

useToggle :: Ui :> es => Bool -> Eff es (Bool, Eff es ())
useToggle initial = do
  (value, setValue) <- useFlag initial
  pure (value, setValue (not value))