packages feed

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

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StrictData #-}

-- | Widget ids and the id context they are derived from. See the
-- "Widget identity" section of "NanoUI" for how ids are assigned.
module NanoUI.Id
  ( WidgetId (..)
  , IdContext (..)
  , initialIdContext
  , idContextWidgetId
  , widgetId
  , hashWidgetId
  , fnv1a
  , mix64
  , mixFnv
  , scopeTag
  , enterScope
  , enterKeyed
  )
where

import Data.Bits (shiftR, xor)
import Data.Char (ord)
import Data.Hashable (Hashable)
import Data.Primitive.Types (Prim)
import Data.Word (Word64, Word8)
import GHC.Stack (HasCallStack, SrcLoc (..), callStack, getCallStack)

newtype WidgetId = WidgetId Word64
  deriving stock (Eq, Ord, Show)
  deriving newtype (Hashable, Prim)

data IdContext = IdContext
  { currentId :: {-# UNPACK #-} !Word64
  , siblingId :: {-# UNPACK #-} !Word64
  }
  deriving stock (Eq, Show)

initialIdContext :: IdContext
initialIdContext = IdContext 0x243F6A8885A308D3 0

-- | Id of the next sibling in this context. A zero hash becomes 1, so
-- @WidgetId 0@ never names a real widget.
{-# INLINE idContextWidgetId #-}
idContextWidgetId :: IdContext -> WidgetId
idContextWidgetId (IdContext cid sid) =
  let
    raw = mix64 cid sid
   in
    if raw == 0 then WidgetId 1 else WidgetId raw

scopeTag :: Word64
scopeTag = 0x9E3779B185EBCA87

keyedTag :: Word64
keyedTag = 0xC2B2AE3D27D4EB4F

{-# INLINE enterScope #-}
enterScope :: Word64 -> IdContext -> (IdContext, IdContext)
enterScope tag parent =
  let
    IdContext pid sib = parent
    child = IdContext (mix64 (mix64 pid sib) tag) 0
    parent' = parent {siblingId = sib + 1}
   in
    (parent', child)

{-# INLINE enterKeyed #-}
enterKeyed :: Word64 -> IdContext -> (IdContext, IdContext)
enterKeyed tag parent =
  let
    IdContext pid sid = parent
    child = IdContext (mix64 (mix64 pid tag) keyedTag) 0
    parent' = parent {siblingId = sid + 1}
   in
    (parent', child)

{-# INLINE widgetId #-}
widgetId :: HasCallStack => WidgetId
widgetId =
  let
    stack = getCallStack callStack
    loc = case stack of
      (_, loc') : _ -> loc'
      [] -> error "widgetId: empty CallStack"
   in
    hashSrcLoc loc

hashSrcLoc :: SrcLoc -> WidgetId
hashSrcLoc
  ( SrcLoc
      { srcLocPackage
      , srcLocModule
      , srcLocFile
      , srcLocStartLine
      , srcLocStartCol
      }
    ) =
    WidgetId $
      fnv1a srcLocPackage
        `mixFnv` fnv1a srcLocModule
        `mixFnv` fnv1a srcLocFile
        `mixFnv` fromIntegral srcLocStartLine
        `mixFnv` fromIntegral srcLocStartCol

{-# INLINE hashWidgetId #-}
hashWidgetId :: WidgetId -> Word64
hashWidgetId (WidgetId w) = w

{-# INLINE fnv1a #-}
fnv1a :: String -> Word64
fnv1a s =
  foldl'
    (\acc c -> (fromIntegral @Word8 @Word64 (fromIntegral (ord c)) `xor` acc) * 0x00000100000001B3)
    0xcbf29ce484222325
    s

{-# INLINE mix64 #-}
mix64 :: Word64 -> Word64 -> Word64
mix64 x y =
  let
    z = x + (y * 0x9E3779B97F4A7C15)
    z1 = z `xor` (z `shiftR` 30)
    z2 = z1 * 0xBF58476D1CE4E5B9
    z3 = z2 `xor` (z2 `shiftR` 27)
   in
    z3 * 0x94D049BB133111EB

{-# INLINE mixFnv #-}
mixFnv :: Word64 -> Word64 -> Word64
mixFnv x y = (x `xor` y) * 1099511628211