packages feed

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

{-# LANGUAGE RecordWildCards #-}

-- | Flat span buffer: strided prim arrays for geometry and colors, boxed texts.
module NanoUI.Frame.SpanArena
  ( SpanArena
  , newSpanArena
  , resetSpanArena
  , pushSpan
  , spanArenaCount
  , spanArenaToList
  , spanArenaToListOccluded
  , foldSpanArena
  ) where

import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import qualified Data.IntMap.Strict as IM
import Data.Primitive.Array (MutableArray, copyMutableArray, newArray, readArray, sizeofMutableArray, writeArray)
import Data.Primitive.PrimArray
  ( MutablePrimArray
  , newPrimArray
  , readPrimArray
  , resizeMutablePrimArray
  , writePrimArray
  )
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word32)
import GHC.Exts (RealWorld)
import NanoUI.Types (Color (..), Rect (..), colorToWord32, rectFullyInside, rectIntersect)

data SpanArena = SpanArena
  { saCount :: IORef Int
  , saArrays :: IORef SpanArenaArrays
  }

-- | Span columns. @saRects@ holds 'rectStride' floats per span (the span rect,
-- then its clip), @saColors@ the foreground and background, and @saTexts@ one
-- text per span; its size is the capacity.
data SpanArenaArrays = SpanArenaArrays
  { saRects :: !(MutablePrimArray RealWorld Float)
  , saColors :: !(MutablePrimArray RealWorld Word32)
  , saTexts :: !(MutableArray RealWorld Text)
  }

rectStride :: Int
rectStride = 8

newSpanArena :: Int -> IO SpanArena
newSpanArena cap0 = do
  let cap = max 16 cap0
  saCount <- newIORef 0
  saRects <- newPrimArray (cap * rectStride)
  saColors <- newPrimArray (cap * 2)
  saTexts <- newArray cap T.empty
  saArrays <- newIORef SpanArenaArrays {..}
  pure SpanArena {..}

resetSpanArena :: SpanArena -> IO ()
resetSpanArena sa = writeIORef (saCount sa) 0

spanArenaCount :: SpanArena -> IO Int
spanArenaCount sa = readIORef (saCount sa)

{-# NOINLINE growSpanArena #-}
growSpanArena :: SpanArena -> SpanArenaArrays -> Int -> IO SpanArenaArrays
growSpanArena sa SpanArenaArrays {saRects = rects, saColors = colors, saTexts = texts} needed = do
  let cap = sizeofMutableArray texts
      newCap = max needed (cap * 2)
  saRects <- resizeMutablePrimArray rects (newCap * rectStride)
  saColors <- resizeMutablePrimArray colors (newCap * 2)
  saTexts <- newArray newCap T.empty
  copyMutableArray saTexts 0 texts 0 cap
  let a = SpanArenaArrays {..}
  writeIORef (saArrays sa) a
  pure a

{-# INLINE pushSpan #-}
pushSpan :: SpanArena -> Rect -> Text -> Color -> Color -> Rect -> IO ()
pushSpan sa (Rect x y w h) txt fg bg (Rect cx cy cw ch) = do
  i <- readIORef (saCount sa)
  a0 <- readIORef (saArrays sa)
  SpanArenaArrays {..} <-
    if i < sizeofMutableArray (saTexts a0) then pure a0 else growSpanArena sa a0 (i + 1)
  let !r = i * rectStride
  writePrimArray saRects r x
  writePrimArray saRects (r + 1) y
  writePrimArray saRects (r + 2) w
  writePrimArray saRects (r + 3) h
  writePrimArray saRects (r + 4) cx
  writePrimArray saRects (r + 5) cy
  writePrimArray saRects (r + 6) cw
  writePrimArray saRects (r + 7) ch
  writePrimArray saColors (2 * i) (colorToWord32 fg)
  writePrimArray saColors (2 * i + 1) (colorToWord32 bg)
  writeArray saTexts i txt
  writeIORef (saCount sa) (i + 1)

spanArenaToList :: SpanArena -> IO [(Rect, Text, Color, Color, Rect)]
spanArenaToList = spanArenaToListOccluded IM.empty

-- | Spans in push order, dropping those hidden behind @panels@.
spanArenaToListOccluded :: IM.IntMap Rect -> SpanArena -> IO [(Rect, Text, Color, Color, Rect)]
spanArenaToListOccluded panels sa =
  foldSpans panels sa True (\acc r t fg bg c -> pure ((r, t, fg, bg, c) : acc)) []

foldSpanArena :: SpanArena -> (Rect -> Text -> Color -> Color -> Rect -> IO ()) -> IO ()
foldSpanArena sa f = foldSpans IM.empty sa False (\_ r t fg bg c -> f r t fg bg c) ()

-- | Fold over the spans not hidden behind @panels@, first to last, or last to
-- first when @backwards@ (so a consing fold builds a list in push order).
{-# INLINE foldSpans #-}
foldSpans ::
  IM.IntMap Rect ->
  SpanArena ->
  Bool ->
  (acc -> Rect -> Text -> Color -> Color -> Rect -> IO acc) ->
  acc ->
  IO acc
foldSpans panels sa backwards f z = do
  n <- readIORef (saCount sa)
  SpanArenaArrays {..} <- readIORef (saArrays sa)
  let panelRects = IM.elems panels
      go !i !acc
        | i < 0 || i >= n = pure acc
        | otherwise = do
            let !r = i * rectStride
            x <- readPrimArray saRects r
            y <- readPrimArray saRects (r + 1)
            w <- readPrimArray saRects (r + 2)
            h <- readPrimArray saRects (r + 3)
            cx <- readPrimArray saRects (r + 4)
            cy <- readPrimArray saRects (r + 5)
            cw <- readPrimArray saRects (r + 6)
            ch <- readPrimArray saRects (r + 7)
            fg <- readPrimArray saColors (2 * i)
            bg <- readPrimArray saColors (2 * i + 1)
            txt <- readArray saTexts i
            let rect = Rect x y w h
                clip = Rect cx cy cw ch
            acc' <-
              if not (null panelRects) && spanOccluded panelRects rect clip
                then pure acc
                else f acc rect txt (Color fg) (Color bg) clip
            go (if backwards then i - 1 else i + 1) acc'
  go (if backwards then n - 1 else 0) z

spanOccluded :: [Rect] -> Rect -> Rect -> Bool
spanOccluded panelRects rect clip =
  case rectIntersect rect clip of
    Nothing -> True
    Just visible -> any (rectFullyInside visible) panelRects