nano-ui-0.1.0.0: lib/NanoUI/Layout/Arena.hs
{-# LANGUAGE RecordWildCards #-}
-- | The node arena: one frame's layout nodes stored column-wise in primitive
-- arrays (geometry, style, tags and tree links), with accessors, traversals
-- and the layout cache.
module NanoUI.Layout.Arena
( NodeIdx
, NodeType (..)
, NodeArenaArrays (..)
, isWidgetNode
, isContainerNode
, isScrollNode
, isFloatingNode
, SizingTag (..)
, DirTag (..)
, NodeArena (..)
, FlexScratch (..)
, newNodeArena
, resetNodeArena
, arenaCount
, topModalNode
, floatingNodeCount
, arenaArrays
, withArenaArraysSnap
, geomX
, geomY
, geomW
, geomH
, styleWVal
, styleHVal
, styleMinW
, styleMinH
, styleMaxW
, styleMaxH
, stylePadL
, stylePadR
, stylePadT
, stylePadB
, styleGap
, styleGridMinColW
, tagNodeType
, tagDirection
, tagWSizing
, tagHSizing
, tagScrollBarSlot
, treeParent
, treeFirstChild
, treeNextSibling
, treeStyleIdx
, treeGridCols
, readGeom
, writeGeom
, readStyle
, readTagEnum
, writeTagEnum
, readTree
, writeTree
, addNode
, addNodeFromLayout
, rootAttachParent
, setNodeText
, getParent
, getFirstChild
, getNextSibling
, getChildCount
, getNodeType
, getDirection
, getGridCols
, getGridMinColW
, getScrollContentW
, setScrollContentW
, getWidthSizing
, getHeightSizing
, getPadding
, getGap
, getMinMax
, parentIsRow
, getAlignX
, getAlignY
, getRect
, setRect
, getLayoutRect
, getClipRect
, setClipRect
, snapshotLayoutRects
, getText
, getOptions
, setOptions
, getWidgetId
, setWidgetId
, lookupNodeByWidgetId
, lookupNodeByKey
, getStyleIdx
, setStyleIdx
, getNodeValue
, setNodeValue
, getNodeFontSize
, getNodeFontColor
, getNodeScope
, getArenaScope
, setArenaScope
, getScopeSignature
, ensureScratchCapacity
, AxisSnapshot (..)
, ensureAxisSnapshot
, memoizeWidth
, forNodes_
, forChildNodes_
, foldFlowChildrenM
, findNodeRevM
, foldNodeRevM
, findNodeM
, foldNodesM
, findChildM
, LayoutCache (..)
, newLayoutCache
, captureLayoutCache
, layoutCacheEligible
, layoutInputsMatch
, restoreLayoutCache
) where
import Control.Exception (bracket_)
import Control.Monad (forM_, when)
import Data.Bits (shiftL, shiftR, xor, (.&.), (.|.))
import Data.HashTable.IO (BasicHashTable)
import qualified Data.HashTable.IO as HT
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Primitive.Array (MutableArray, copyMutableArray, newArray, readArray, sizeofMutableArray, writeArray)
import Data.Primitive.PrimArray
( MutablePrimArray
, copyMutablePrimArray
, newPrimArray
, readPrimArray
, setPrimArray
, writePrimArray
, resizeMutablePrimArray
)
import Data.Primitive.Types (Prim)
import GHC.Exts (RealWorld)
import Data.Text (Text)
import Data.Word (Word8, Word32, Word64)
import qualified Data.Text as T
import NanoUI.Id (WidgetId (..), hashWidgetId)
import NanoUI.Style (AlignX, AlignY, Direction (..), Layout (..), Padding (..), Sizing (..))
import NanoUI.Types (Color (..), Rect (..))
type NodeIdx = Int
data NodeType
= NodeContainer
| NodeText
| NodeSpacer
| NodeSeparator
| NodeWidget
| NodeButton
| NodeCheckbox
| NodeSlider
| NodeTextInput
| NodeTextArea
| NodeScrollContainer
| NodeSelect
| NodeModal
| NodeImage
| NodePanel
| NodeWindow
-- Appended last: stored as Word8 in the arena. Update every exhaustive
-- NodeType case when adding variants.
| NodeBox
| NodeRadio
| NodeColorPicker
| NodeTree
| NodePopup
| NodeDrawing
deriving (Eq, Show, Enum, Bounded)
isWidgetNode :: NodeType -> Bool
isWidgetNode nt =
case nt of
NodeWidget -> True
NodeButton -> True
NodeCheckbox -> True
NodeRadio -> True
NodeSlider -> True
NodeTextInput -> True
NodeTextArea -> True
NodeSelect -> True
NodeColorPicker -> True
NodeTree -> True
NodeDrawing -> True
_ -> False
isContainerNode :: NodeType -> Bool
isContainerNode nt =
case nt of
NodeContainer -> True
NodeScrollContainer -> True
NodeModal -> True
NodePanel -> True
NodeWindow -> True
NodePopup -> True
_ -> False
isScrollNode :: NodeType -> Bool
isScrollNode nt = nt == NodeScrollContainer
isFloatingNode :: NodeType -> Bool
isFloatingNode nt = nt == NodeModal || nt == NodeWindow || nt == NodePopup
data SizingTag
= SizingFixed
| SizingFit
| SizingGrow
| SizingShrink
| SizingPercent
deriving (Eq, Show, Enum, Bounded)
data DirTag = DirRow | DirColumn
deriving (Eq, Show, Enum, Bounded)
-- | Node columns. Each array holds one row of @*Stride@ slots per node; the
-- column constants below name the slots.
data NodeArenaArrays = NodeArenaArrays
{ naArrGeom :: !(MutablePrimArray RealWorld Float)
, naArrStyle :: !(MutablePrimArray RealWorld Float)
, naArrTags :: !(MutablePrimArray RealWorld Word8)
, naArrTree :: !(MutablePrimArray RealWorld Int)
, naArrTextStore :: !(MutableArray RealWorld Text)
, naArrOptionsStore :: !(MutableArray RealWorld [Text])
, naArrFontColor :: !(MutablePrimArray RealWorld Int)
, naArrScope :: !(MutablePrimArray RealWorld Int)
-- ^ The paint scope each node was added under: a theme index shifted left
-- one bit, and the disabled flag in bit 0. See
-- 'NanoUI.Context.Types.ThemeScopes'.
}
data NodeArena = NodeArena
{ naCount :: IORef Int
, naCapacity :: IORef Int
, naArrays :: IORef NodeArenaArrays
, naArraysSnap :: IORef (Maybe NodeArenaArrays)
, naScratch :: IORef FlexScratch
-- Per-depth copies of the axis scratch while the position pass recurses.
-- Children reuse the working scratch, so a container's child list must be
-- snapshotted at its own depth to survive recursive positioning.
, naSnapCap :: IORef Int
, naSnapLevels :: IORef (MutableArray RealWorld (Maybe AxisSnapshot))
-- Per-frame memos keyed by (node, quantized width): wrapped text sizes and
-- fit heights. Text and style are fixed per node within a frame, so the
-- frame tag is all that is needed to invalidate across frames.
, naFrameTag :: IORef Word32
, naWrapMemo :: IORef WidthMemo
, naFitMemo :: IORef WidthMemo
, naEpoch :: IORef Word32
, naIndex :: IORef (BasicHashTable WidgetId Word64)
-- The scope new nodes are stamped with, and a signature of the scoped
-- nodes added this frame, so a frame that only changes scopes can tell.
, naScope :: IORef Int
, naScopeSig :: IORef Word64
, naTopModal :: IORef Int
-- ^ Index of the last modal node added this frame, or -1. Node types are
-- fixed when a node is added and indices only grow until a reset, so this
-- is the topmost modal without a scan.
, naFloatingCount :: IORef Int
-- ^ Floating nodes (windows, modals, popups) added this frame.
}
-- | Flex solver scratch: child node indices, their measured widths and
-- heights, and the distributed output sizes.
data FlexScratch = FlexScratch
{ fsCap :: !Int
, fsIdx :: !(MutablePrimArray RealWorld Int)
, fsW :: !(MutablePrimArray RealWorld Float)
, fsH :: !(MutablePrimArray RealWorld Float)
, fsOutW :: !(MutablePrimArray RealWorld Float)
, fsOutH :: !(MutablePrimArray RealWorld Float)
}
-- | A per-frame memo of two floats per node keyed by a width. Slots hold
-- @(key, a, b)@ per node; an entry is live only while its tag equals the
-- arena's frame tag.
data WidthMemo = WidthMemo
{ wmTags :: !(MutablePrimArray RealWorld Word32)
, wmSlots :: !(MutablePrimArray RealWorld Float)
}
-- | Initial number of per-depth layout snapshot levels. The level array grows
-- on demand (see 'ensureSnapLevelsArr'), so this is not a depth limit.
maxSnapDepth :: Int
maxSnapDepth = 256
-- | One depth level's frozen child indices and distributed main-axis sizes.
data AxisSnapshot = AxisSnapshot
{ asIdx :: !(MutablePrimArray RealWorld Int)
, asOut :: !(MutablePrimArray RealWorld Float)
}
initialCapacity :: Int
initialCapacity = 256
-- | Geometry columns: solved rect, the position snapshot taken by
-- 'snapshotLayoutRects', and the clip rect.
geomStride, geomX, geomY, geomW, geomH, geomLayoutX, geomLayoutY :: Int
geomStride = 10
geomX = 0
geomY = 1
geomW = 2
geomH = 3
geomLayoutX = 4
geomLayoutY = 5
geomClipX, geomClipY, geomClipW, geomClipH :: Int
geomClipX = 6
geomClipY = 7
geomClipW = 8
geomClipH = 9
-- | Style columns: sizing values, padding, gap, min/max, grow, and per-node
-- values that are not layout inputs (scroll extent, node value, font size).
styleStride, styleWVal, styleHVal, stylePadL, stylePadR, stylePadT, stylePadB :: Int
styleStride = 16
styleWVal = 0
styleHVal = 1
stylePadL = 2
stylePadR = 3
stylePadT = 4
stylePadB = 5
styleGap, styleMinW, styleMinH, styleMaxW, styleMaxH, styleGrow :: Int
styleGap = 6
styleMinW = 7
styleMinH = 8
styleMaxW = 9
styleMaxH = 10
styleGrow = 11
styleScrollContentW, styleNodeValue, styleGridMinColW, styleFontSize :: Int
styleScrollContentW = 12
styleNodeValue = 13
styleGridMinColW = 14
styleFontSize = 15
-- | Tag columns (enum values as 'Word8'). Column 7 is unused. The scrollbar
-- slot is a solver output: measurement writes it for scroll containers, so
-- the layout cache skips it when comparing inputs and restores it on a hit.
tagStride, tagNodeType, tagDirection, tagWSizing, tagHSizing, tagScrollBarSlot, tagAlignX, tagAlignY :: Int
tagStride = 8 -- a power of two: layoutInputsMatch masks by it
tagNodeType = 0
tagDirection = 1
tagWSizing = 2
tagHSizing = 3
tagScrollBarSlot = 4
tagAlignX = 5
tagAlignY = 6
-- | Tree columns: links, widget id, style index, text index (-1 for no text),
-- and the grid column count (containers only).
treeStride, treeParent, treeFirstChild, treeNextSibling, treeChildCount :: Int
treeStride = 8
treeParent = 0
treeFirstChild = 1
treeNextSibling = 2
treeChildCount = 3
treeWidgetId, treeStyleIdx, treeTextIdx, treeGridCols :: Int
treeWidgetId = 4
treeStyleIdx = 5
treeTextIdx = 6
treeGridCols = 7
{-# INLINE readGeom #-}
readGeom :: NodeArenaArrays -> NodeIdx -> Int -> IO Float
readGeom a idx col = readPrimArray (naArrGeom a) (idx * geomStride + col)
{-# INLINE writeGeom #-}
writeGeom :: NodeArenaArrays -> NodeIdx -> Int -> Float -> IO ()
writeGeom a idx col = writePrimArray (naArrGeom a) (idx * geomStride + col)
{-# INLINE readStyle #-}
readStyle :: NodeArenaArrays -> NodeIdx -> Int -> IO Float
readStyle a idx col = readPrimArray (naArrStyle a) (idx * styleStride + col)
{-# INLINE writeStyle #-}
writeStyle :: NodeArenaArrays -> NodeIdx -> Int -> Float -> IO ()
writeStyle a idx col = writePrimArray (naArrStyle a) (idx * styleStride + col)
{-# INLINE readTagEnum #-}
readTagEnum :: Enum e => NodeArenaArrays -> NodeIdx -> Int -> IO e
readTagEnum a idx col = do
t <- readPrimArray (naArrTags a) (idx * tagStride + col)
pure $! toEnum (fromIntegral t)
{-# INLINE writeTagEnum #-}
writeTagEnum :: Enum e => NodeArenaArrays -> NodeIdx -> Int -> e -> IO ()
writeTagEnum a idx col v = writePrimArray (naArrTags a) (idx * tagStride + col) (fromIntegral (fromEnum v))
{-# INLINE readTree #-}
readTree :: NodeArenaArrays -> NodeIdx -> Int -> IO Int
readTree a idx col = readPrimArray (naArrTree a) (idx * treeStride + col)
{-# INLINE writeTree #-}
writeTree :: NodeArenaArrays -> NodeIdx -> Int -> Int -> IO ()
writeTree a idx col = writePrimArray (naArrTree a) (idx * treeStride + col)
newNodeArenaArrays :: Int -> IO NodeArenaArrays
newNodeArenaArrays cap = do
naArrGeom <- newPrimArray (cap * geomStride)
naArrStyle <- newPrimArray (cap * styleStride)
naArrTags <- newPrimArray (cap * tagStride)
naArrTree <- newPrimArray (cap * treeStride)
naArrTextStore <- newArray cap T.empty
naArrOptionsStore <- newArray cap []
naArrFontColor <- newPrimArray cap
naArrScope <- newPrimArray cap
pure NodeArenaArrays {..}
newFlexScratch :: Int -> IO FlexScratch
newFlexScratch fsCap = do
fsIdx <- newPrimArray fsCap
fsW <- newPrimArray fsCap
fsH <- newPrimArray fsCap
fsOutW <- newPrimArray fsCap
fsOutH <- newPrimArray fsCap
pure FlexScratch {..}
-- | Memo slots per node: key and two values.
memoStride :: Int
memoStride = 3
-- Tags start zeroed: frame tags are never 0, so fresh entries always miss.
newWidthMemo :: Int -> IO WidthMemo
newWidthMemo cap = do
wmTags <- newPrimArray cap
setPrimArray wmTags 0 cap 0
wmSlots <- newPrimArray (cap * memoStride)
pure WidthMemo {..}
newNodeArena :: IO NodeArena
newNodeArena = do
let cap = initialCapacity
scratchCap = 64
naCount <- newIORef 0
naCapacity <- newIORef cap
naArrays <- newIORef =<< newNodeArenaArrays cap
naArraysSnap <- newIORef Nothing
naScratch <- newIORef =<< newFlexScratch scratchCap
naSnapCap <- newIORef scratchCap
naSnapLevels <- newIORef =<< newArray maxSnapDepth Nothing
naFrameTag <- newIORef 1
naWrapMemo <- newIORef =<< newWidthMemo cap
naFitMemo <- newIORef =<< newWidthMemo cap
naEpoch <- newIORef 1
naIndex <- newIORef =<< HT.new
naScope <- newIORef 0
naScopeSig <- newIORef 0
naTopModal <- newIORef (-1)
naFloatingCount <- newIORef 0
pure NodeArena {..}
resetNodeArena :: NodeArena -> IO ()
resetNodeArena na = do
writeIORef (naCount na) 0
writeIORef (naScope na) 0
writeIORef (naScopeSig na) 0
writeIORef (naTopModal na) (-1)
writeIORef (naFloatingCount na) 0
!ft <- readIORef (naFrameTag na)
writeIORef (naFrameTag na) (if ft == maxBound then 1 else ft + 1)
!ep <- readIORef (naEpoch na)
let !ep' = ep + 1
if ep' == 0 || (ep' .&. 0x7F == 0)
then do
let !nextEp = if ep' == 0 then 1 else ep'
writeIORef (naEpoch na) nextEp
writeIORef (naIndex na) =<< HT.new
else writeIORef (naEpoch na) ep'
-- | The topmost (last added) modal node, if any.
{-# INLINE topModalNode #-}
topModalNode :: NodeArena -> IO (Maybe NodeIdx)
topModalNode na = do
i <- readIORef (naTopModal na)
pure (if i >= 0 then Just i else Nothing)
{-# INLINE floatingNodeCount #-}
floatingNodeCount :: NodeArena -> IO Int
floatingNodeCount na = readIORef (naFloatingCount na)
{-# INLINE arenaCount #-}
arenaCount :: NodeArena -> IO Int
arenaCount na = readIORef (naCount na)
{-# INLINE arenaArrays #-}
arenaArrays :: NodeArena -> IO NodeArenaArrays
arenaArrays na = do
m <- readIORef (naArraysSnap na)
case m of
Just a -> pure a
Nothing -> readIORef (naArrays na)
-- | Pin arena column arrays for a layout pass so field reads skip naArrays IORef.
withArenaArraysSnap :: NodeArena -> IO a -> IO a
withArenaArraysSnap na act =
bracket_
(readIORef (naArrays na) >>= writeIORef (naArraysSnap na) . Just)
(writeIORef (naArraysSnap na) Nothing)
act
{-# NOINLINE ensureCapacity #-}
ensureCapacity :: NodeArena -> Int -> IO ()
ensureCapacity na needed = do
cap <- readIORef (naCapacity na)
if needed < cap
then pure ()
else do
let newCap = cap * 2
newA <- readIORef (naArrays na) >>= growNodeArenaArrays cap newCap
growWidthMemo (naWrapMemo na) cap newCap
growWidthMemo (naFitMemo na) cap newCap
writeIORef (naArrays na) newA
m <- readIORef (naArraysSnap na)
case m of
Just{} -> writeIORef (naArraysSnap na) (Just newA)
Nothing -> pure ()
writeIORef (naCapacity na) newCap
-- | Copy of @a@ with room for @newCap@ nodes; new slots are zero or empty.
growNodeArenaArrays :: Int -> Int -> NodeArenaArrays -> IO NodeArenaArrays
growNodeArenaArrays cap newCap a = do
naArrGeom <- growPrimArrayCopy (naArrGeom a) (cap * geomStride) (newCap * geomStride) 0
naArrStyle <- growPrimArrayCopy (naArrStyle a) (cap * styleStride) (newCap * styleStride) 0
naArrTags <- growPrimArrayCopy (naArrTags a) (cap * tagStride) (newCap * tagStride) 0
naArrTree <- growPrimArrayCopy (naArrTree a) (cap * treeStride) (newCap * treeStride) 0
naArrTextStore <- growBoxedStoreCopy T.empty (naArrTextStore a) cap newCap
naArrOptionsStore <- growBoxedStoreCopy [] (naArrOptionsStore a) cap newCap
naArrFontColor <- growPrimArrayCopy (naArrFontColor a) cap newCap 0
naArrScope <- growPrimArrayCopy (naArrScope a) cap newCap 0
pure NodeArenaArrays {..}
{-# NOINLINE growPrimArrayCopy #-}
growPrimArrayCopy :: Prim a => MutablePrimArray RealWorld a -> Int -> Int -> a -> IO (MutablePrimArray RealWorld a)
growPrimArrayCopy oldArr cap newCap defVal = do
newArr <- resizeMutablePrimArray oldArr newCap
setPrimArray newArr cap (newCap - cap) defVal
pure newArr
growWidthMemo :: IORef WidthMemo -> Int -> Int -> IO ()
growWidthMemo ref cap newCap = do
WidthMemo tags slots <- readIORef ref
wmTags <- growPrimArrayCopy tags cap newCap 0
wmSlots <- growPrimArrayCopy slots (cap * memoStride) (newCap * memoStride) 0
writeIORef ref WidthMemo {..}
{-# NOINLINE growBoxedStoreCopy #-}
growBoxedStoreCopy :: a -> MutableArray RealWorld a -> Int -> Int -> IO (MutableArray RealWorld a)
growBoxedStoreCopy emptyVal arr oldCap newCap = do
newArr <- newArray newCap emptyVal
copyMutableArray newArr 0 arr 0 oldCap
pure newArr
{-# INLINE sizingTag #-}
sizingTag :: Sizing -> (SizingTag, Float)
sizingTag (Fixed v) = (SizingFixed, v)
sizingTag Fit = (SizingFit, 0)
sizingTag (Grow g) = (SizingGrow, g)
sizingTag (Shrink s) = (SizingShrink, s)
sizingTag (Percent p) = (SizingPercent, p)
-- Empty stack attaches to node 0 so walks from the page root still reach
-- windows/modals/popups built as UI siblings.
rootAttachParent :: NodeArena -> Int -> IO Int
rootAttachParent na parent
| parent >= 0 = pure parent
| otherwise = do
n <- arenaCount na
pure (if n > 0 then 0 else -1)
{-# INLINE addNode #-}
addNode ::
NodeArena ->
NodeType ->
Int ->
Direction ->
Sizing ->
Sizing ->
Padding ->
Float ->
Float ->
Float ->
Float ->
Float ->
Float ->
AlignX ->
AlignY ->
IO NodeIdx
addNode na nt parent dir wSiz hSiz pad gap minW minH maxW maxH grow ax ay = do
idx <- readIORef (naCount na)
ensureCapacity na (idx + 1)
let (wTag, wVal) = sizingTag wSiz
(hTag, hVal) = sizingTag hSiz
a <- arenaArrays na
setPrimArray (naArrGeom a) (idx * geomStride) geomStride 0
writeStyle a idx styleWVal wVal
writeStyle a idx styleHVal hVal
writeStyle a idx stylePadL (padL pad)
writeStyle a idx stylePadR (padR pad)
writeStyle a idx stylePadT (padT pad)
writeStyle a idx stylePadB (padB pad)
writeStyle a idx styleGap gap
writeStyle a idx styleMinW minW
writeStyle a idx styleMinH minH
writeStyle a idx styleMaxW maxW
writeStyle a idx styleMaxH maxH
writeStyle a idx styleGrow grow
setPrimArray (naArrStyle a) (idx * styleStride + styleScrollContentW) (styleStride - styleScrollContentW) 0
setPrimArray (naArrTags a) (idx * tagStride) tagStride 0
writeTagEnum a idx tagNodeType nt
writeTagEnum a idx tagDirection $ case dir of
Row -> DirRow
Column -> DirColumn
writeTagEnum a idx tagWSizing wTag
writeTagEnum a idx tagHSizing hTag
writeTagEnum a idx tagAlignX ax
writeTagEnum a idx tagAlignY ay
setPrimArray (naArrTree a) (idx * treeStride) treeStride 0
writeTree a idx treeParent parent
writeTree a idx treeFirstChild (-1)
writeTree a idx treeNextSibling (-1)
writeTree a idx treeTextIdx (-1)
writePrimArray (naArrFontColor a) idx 0
scope <- readIORef (naScope na)
writePrimArray (naArrScope a) idx scope
when (scope /= 0) $ do
sig <- readIORef (naScopeSig na)
writeIORef (naScopeSig na) $! (sig * 0x100000001b3) `xor` (fromIntegral idx `shiftL` 32 .|. fromIntegral scope)
writeArray (naArrOptionsStore a) idx []
when (parent >= 0) $ do
fc <- readTree a parent treeFirstChild
writeTree a idx treeNextSibling fc
writeTree a parent treeFirstChild idx
cc <- readTree a parent treeChildCount
writeTree a parent treeChildCount (cc + 1)
when (isFloatingNode nt) $ do
when (nt == NodeModal) $ writeIORef (naTopModal na) idx
fc <- readIORef (naFloatingCount na)
writeIORef (naFloatingCount na) (fc + 1)
writeIORef (naCount na) (idx + 1)
pure idx
addNodeFromLayout :: NodeArena -> NodeType -> Int -> Layout -> IO NodeIdx
addNodeFromLayout na nt parent l = do
idx <-
addNode
na
nt
parent
(layoutDirection l)
(layoutWidth l)
(layoutHeight l)
(layoutPadding l)
(layoutGap l)
(layoutMinW l)
(layoutMinH l)
(layoutMaxW l)
(layoutMaxH l)
0
(layoutAlignX l)
(layoutAlignY l)
setGridCols na idx (layoutGridCols l)
setGridMinColW na idx (layoutGridMinColW l)
setNodeFontSize na idx (layoutFontSize l)
setNodeFontColor na idx (layoutFontColor l)
pure idx
{-# INLINE setNodeText #-}
setNodeText :: NodeArena -> NodeIdx -> Text -> IO ()
setNodeText na idx txt = do
a <- arenaArrays na
writeArray (naArrTextStore a) idx txt
writeTree a idx treeTextIdx idx
{-# INLINE getParent #-}
getParent :: NodeArena -> NodeIdx -> IO NodeIdx
getParent na idx = arenaArrays na >>= \a -> readTree a idx treeParent
{-# INLINE getFirstChild #-}
getFirstChild :: NodeArena -> NodeIdx -> IO NodeIdx
getFirstChild na idx = arenaArrays na >>= \a -> readTree a idx treeFirstChild
{-# INLINE getNextSibling #-}
getNextSibling :: NodeArena -> NodeIdx -> IO NodeIdx
getNextSibling na idx = arenaArrays na >>= \a -> readTree a idx treeNextSibling
{-# INLINE getChildCount #-}
getChildCount :: NodeArena -> NodeIdx -> IO Int
getChildCount na idx = arenaArrays na >>= \a -> readTree a idx treeChildCount
{-# INLINE getNodeType #-}
getNodeType :: NodeArena -> NodeIdx -> IO NodeType
getNodeType na idx = arenaArrays na >>= \a -> readTagEnum a idx tagNodeType
{-# INLINE getDirection #-}
getDirection :: NodeArena -> NodeIdx -> IO DirTag
getDirection na idx = arenaArrays na >>= \a -> readTagEnum a idx tagDirection
{-# INLINE getGridCols #-}
getGridCols :: NodeArena -> NodeIdx -> IO Int
getGridCols na idx = arenaArrays na >>= \a -> readTree a idx treeGridCols
{-# INLINE setGridCols #-}
setGridCols :: NodeArena -> NodeIdx -> Int -> IO ()
setGridCols na idx c = arenaArrays na >>= \a -> writeTree a idx treeGridCols c
{-# INLINE getWidthSizing #-}
getWidthSizing :: NodeArena -> NodeIdx -> IO (SizingTag, Float)
getWidthSizing na idx = arenaArrays na >>= \a -> (,) <$> readTagEnum a idx tagWSizing <*> readStyle a idx styleWVal
{-# INLINE getHeightSizing #-}
getHeightSizing :: NodeArena -> NodeIdx -> IO (SizingTag, Float)
getHeightSizing na idx = arenaArrays na >>= \a -> (,) <$> readTagEnum a idx tagHSizing <*> readStyle a idx styleHVal
{-# INLINE getPadding #-}
getPadding :: NodeArena -> NodeIdx -> IO Padding
getPadding na idx = do
a <- arenaArrays na
Padding <$> readStyle a idx stylePadL <*> readStyle a idx stylePadR <*> readStyle a idx stylePadT <*> readStyle a idx stylePadB
{-# INLINE getGap #-}
getGap :: NodeArena -> NodeIdx -> IO Float
getGap na idx = arenaArrays na >>= \a -> readStyle a idx styleGap
{-# INLINE getMinMax #-}
getMinMax :: NodeArena -> NodeIdx -> IO (Float, Float, Float, Float)
getMinMax na idx = do
a <- arenaArrays na
(,,,) <$> readStyle a idx styleMinW <*> readStyle a idx styleMinH <*> readStyle a idx styleMaxW <*> readStyle a idx styleMaxH
{-# INLINE getScrollContentW #-}
getScrollContentW :: NodeArena -> NodeIdx -> IO Float
getScrollContentW na idx = arenaArrays na >>= \a -> readStyle a idx styleScrollContentW
{-# INLINE setScrollContentW #-}
setScrollContentW :: NodeArena -> NodeIdx -> Float -> IO ()
setScrollContentW na idx v = arenaArrays na >>= \a -> writeStyle a idx styleScrollContentW v
{-# INLINE getGridMinColW #-}
getGridMinColW :: NodeArena -> NodeIdx -> IO Float
getGridMinColW na idx = arenaArrays na >>= \a -> readStyle a idx styleGridMinColW
{-# INLINE setGridMinColW #-}
setGridMinColW :: NodeArena -> NodeIdx -> Float -> IO ()
setGridMinColW na idx v = arenaArrays na >>= \a -> writeStyle a idx styleGridMinColW v
{-# INLINE parentIsRow #-}
parentIsRow :: NodeArena -> NodeIdx -> IO Bool
parentIsRow na idx = do
p <- getParent na idx
if p < 0
then pure False
else do
dir <- getDirection na p
pure (dir == DirRow)
{-# INLINE getAlignX #-}
getAlignX :: NodeArena -> NodeIdx -> IO AlignX
getAlignX na idx = arenaArrays na >>= \a -> readTagEnum a idx tagAlignX
{-# INLINE getAlignY #-}
getAlignY :: NodeArena -> NodeIdx -> IO AlignY
getAlignY na idx = arenaArrays na >>= \a -> readTagEnum a idx tagAlignY
{-# INLINE getRect #-}
getRect :: NodeArena -> NodeIdx -> IO (Float, Float, Float, Float)
getRect na idx = do
a <- arenaArrays na
(,,,) <$> readGeom a idx geomX <*> readGeom a idx geomY <*> readGeom a idx geomW <*> readGeom a idx geomH
{-# INLINE setRect #-}
setRect :: NodeArena -> NodeIdx -> Float -> Float -> Float -> Float -> IO ()
setRect na idx x y w h = do
a <- arenaArrays na
writeGeom a idx geomX x
writeGeom a idx geomY y
writeGeom a idx geomW w
writeGeom a idx geomH h
{-# INLINE getLayoutRect #-}
getLayoutRect :: NodeArena -> NodeIdx -> IO (Float, Float, Float, Float)
getLayoutRect na idx = do
a <- arenaArrays na
(,,,) <$> readGeom a idx geomLayoutX <*> readGeom a idx geomLayoutY <*> readGeom a idx geomW <*> readGeom a idx geomH
{-# INLINE getClipRect #-}
getClipRect :: NodeArena -> NodeIdx -> IO (Maybe Rect)
getClipRect na idx = do
a <- arenaArrays na
x <- readGeom a idx geomClipX
y <- readGeom a idx geomClipY
w <- readGeom a idx geomClipW
h <- readGeom a idx geomClipH
let r = Rect x y w h
pure (if w > 0 && h > 0 then Just r else Nothing)
{-# INLINE setClipRect #-}
setClipRect :: NodeArena -> NodeIdx -> Rect -> IO ()
setClipRect na idx (Rect x y w h) = do
a <- arenaArrays na
writeGeom a idx geomClipX x
writeGeom a idx geomClipY y
writeGeom a idx geomClipW w
writeGeom a idx geomClipH h
{-# INLINE snapshotLayoutRects #-}
snapshotLayoutRects :: NodeArena -> IO ()
snapshotLayoutRects na = do
a <- arenaArrays na
forNodes_ na $ \i -> do
readGeom a i geomX >>= writeGeom a i geomLayoutX
readGeom a i geomY >>= writeGeom a i geomLayoutY
-- | Cached layout signature and solved geometry for whole-layout reuse. The
-- backing arrays are reused; only cache misses capture a new solved frame.
-- The font colour and scope columns are paint state and stay unused.
data LayoutCache = LayoutCache
{ lcCap :: !Int
, lcCount :: !Int
, lcArrays :: !NodeArenaArrays
}
newLayoutCache :: Int -> IO LayoutCache
newLayoutCache cap0 = do
let !cap = max 16 cap0
LayoutCache cap 0 <$> newNodeArenaArrays cap
-- | Snapshot the current (post-solve) arena form, constraints and rects.
captureLayoutCache :: NodeArena -> LayoutCache -> IO LayoutCache
captureLayoutCache na lc0 = do
n <- arenaCount na
let !oldCap = lcCap lc0
!newCap = max n (oldCap * 2)
lc <-
if n <= oldCap
then pure lc0
else LayoutCache newCap (lcCount lc0) <$> growNodeArenaArrays oldCap newCap (lcArrays lc0)
a <- arenaArrays na
let c = lcArrays lc
copyMutablePrimArray (naArrGeom c) 0 (naArrGeom a) 0 (n * geomStride)
copyMutablePrimArray (naArrStyle c) 0 (naArrStyle a) 0 (n * styleStride)
copyMutablePrimArray (naArrTags c) 0 (naArrTags a) 0 (n * tagStride)
copyMutablePrimArray (naArrTree c) 0 (naArrTree a) 0 (n * treeStride)
copyMutableArray (naArrTextStore c) 0 (naArrTextStore a) 0 n
copyMutableArray (naArrOptionsStore c) 0 (naArrOptionsStore a) 0 n
pure lc {lcCount = n}
-- | Floating placement depends on state outside the arena descriptor. Custom
-- measurement is checked separately by Frame, which owns its registration.
layoutCacheEligible :: NodeArena -> IO Bool
layoutCacheEligible na = do
n <- arenaCount na
a <- arenaArrays na
if n <= 0
then pure False
else allRangeM 0 n $ \i -> not . isFloatingNode <$> readTagEnum a i tagNodeType
-- | Compare layout inputs, stopping at the first mismatch. Node values are
-- paint state except on scroll containers, where they are solver outputs.
-- Neither belongs in the layout-input signature.
layoutInputsMatch :: NodeArena -> LayoutCache -> IO Bool
layoutInputsMatch na lc = do
n <- arenaCount na
if n <= 0 || n /= lcCount lc
then pure False
else do
-- The cache only holds eligible layouts, and matching node types
-- keep the current one eligible too.
a <- arenaArrays na
let c = lcArrays lc
andThen (styleMatch (naArrStyle a) (naArrStyle c) n) $
andThen (allRangeM 0 (n * tagStride) (\k -> if k .&. (tagStride - 1) == tagScrollBarSlot then pure True else primEqAt (naArrTags a) (naArrTags c) k)) $
andThen (treeMatch a (naArrTree c) n) $
andThen (allRangeM 0 n (boxedEqAt (naArrTextStore a) (naArrTextStore c))) $
allRangeM 0 n (boxedEqAt (naArrOptionsStore a) (naArrOptionsStore c))
{-# INLINE andThen #-}
andThen :: IO Bool -> IO Bool -> IO Bool
andThen check next = do
ok <- check
if ok then next else pure False
-- | Whether @p@ holds at every index in @[lo, hi)@, stopping at the first miss.
{-# INLINE allRangeM #-}
allRangeM :: Int -> Int -> (Int -> IO Bool) -> IO Bool
allRangeM lo hi p = go lo
where
go !i
| i >= hi = pure True
| otherwise = do
ok <- p i
if ok then go (i + 1) else pure False
{-# INLINE primEqAt #-}
primEqAt :: (Prim a, Eq a) => MutablePrimArray RealWorld a -> MutablePrimArray RealWorld a -> Int -> IO Bool
primEqAt x y i = (==) <$> readPrimArray x i <*> readPrimArray y i
{-# INLINE boxedEqAt #-}
boxedEqAt :: Eq a => MutableArray RealWorld a -> MutableArray RealWorld a -> Int -> IO Bool
boxedEqAt x y i = (==) <$> readArray x i <*> readArray y i
-- The scroll-extent and node-value columns hold solver outputs or paint-only
-- values, so they are skipped.
styleMatch :: MutablePrimArray RealWorld Float -> MutablePrimArray RealWorld Float -> Int -> IO Bool
styleMatch x y n =
allRangeM 0 n $ \i ->
let !base = i * styleStride
in andThen (allRangeM base (base + styleScrollContentW) (primEqAt x y)) $
allRangeM (base + styleGridMinColW) (base + styleStride) (primEqAt x y)
-- Box/image/drawing style IDs are paint data; their intrinsic dimensions come
-- from sizing constraints. The grid column count only matters to containers.
treeMatch :: NodeArenaArrays -> MutablePrimArray RealWorld Int -> Int -> IO Bool
treeMatch a cached n =
allRangeM 0 n $ \i -> do
nt <- readTagEnum a i tagNodeType
let paintStyle = nt == NodeBox || nt == NodeImage || nt == NodeDrawing
!base = i * treeStride
allRangeM 0 treeStride $ \j ->
if (j == treeStyleIdx && paintStyle) || (j == treeGridCols && not (isContainerNode nt))
then pure True
else (==) <$> readTree a i j <*> readPrimArray cached (base + j)
-- | Restore only solver outputs. Rebuilt paint values/colors must survive a
-- cache hit; copying the entire cached style array would revert them.
restoreLayoutCache :: NodeArena -> LayoutCache -> IO ()
restoreLayoutCache na lc = do
a <- arenaArrays na
let !n = lcCount lc
c = lcArrays lc
copyMutablePrimArray (naArrGeom a) 0 (naArrGeom c) 0 (n * geomStride)
let go !i
| i >= n = pure ()
| otherwise = do
nt <- readTagEnum a i tagNodeType
-- Scroll content width, node value (the content height) and
-- scrollbar slot.
when (isScrollNode nt) $ do
let !off = i * styleStride + styleScrollContentW
!slotOff = i * tagStride + tagScrollBarSlot
copyMutablePrimArray (naArrStyle a) off (naArrStyle c) off 2
readPrimArray (naArrTags c) slotOff >>= writePrimArray (naArrTags a) slotOff
go (i + 1)
go 0
{-# INLINE getText #-}
getText :: NodeArena -> NodeIdx -> IO Text
getText na idx = do
a <- arenaArrays na
ti <- readTree a idx treeTextIdx
if ti < 0
then pure T.empty
else readArray (naArrTextStore a) ti
{-# INLINE getOptions #-}
getOptions :: NodeArena -> NodeIdx -> IO [Text]
getOptions na idx = do
a <- arenaArrays na
readArray (naArrOptionsStore a) idx
{-# INLINE setOptions #-}
setOptions :: NodeArena -> NodeIdx -> [Text] -> IO ()
setOptions na idx opts = do
a <- arenaArrays na
writeArray (naArrOptionsStore a) idx opts
{-# INLINE getWidgetId #-}
getWidgetId :: NodeArena -> NodeIdx -> IO WidgetId
getWidgetId na idx = arenaArrays na >>= \a -> WidgetId . fromIntegral <$> readTree a idx treeWidgetId
{-# INLINE packEpochNode #-}
packEpochNode :: Word32 -> NodeIdx -> Word64
packEpochNode !epoch !idx = (fromIntegral epoch `shiftL` 32) .|. (fromIntegral idx .&. 0xFFFFFFFF)
{-# INLINE unpackEpochNode #-}
unpackEpochNode :: Word64 -> (Word32, NodeIdx)
unpackEpochNode !w = (fromIntegral (w `shiftR` 32), fromIntegral (w .&. 0xFFFFFFFF))
{-# INLINE setWidgetId #-}
setWidgetId :: NodeArena -> NodeIdx -> WidgetId -> IO ()
setWidgetId na idx wid = do
a <- arenaArrays na
let WidgetId w = wid
writeTree a idx treeWidgetId (fromIntegral w)
when (hashWidgetId wid /= 0) $ do
!ep <- readIORef (naEpoch na)
table <- readIORef (naIndex na)
HT.insert table wid (packEpochNode ep idx)
{-# INLINE lookupNodeByWidgetId #-}
lookupNodeByWidgetId :: NodeArena -> WidgetId -> IO (Maybe NodeIdx)
lookupNodeByWidgetId na wid
| hashWidgetId wid == 0 = pure Nothing
| otherwise = do
table <- readIORef (naIndex na)
mVal <- HT.lookup table wid
case mVal of
Nothing -> pure Nothing
Just val -> do
!ep <- readIORef (naEpoch na)
let (!entryEp, !idx) = unpackEpochNode val
pure (if entryEp == ep then Just idx else Nothing)
{-# INLINE lookupNodeByKey #-}
lookupNodeByKey :: NodeArena -> Int -> IO (Maybe NodeIdx)
lookupNodeByKey na key = lookupNodeByWidgetId na (WidgetId (fromIntegral key))
{-# INLINE getNodeValue #-}
getNodeValue :: NodeArena -> NodeIdx -> IO Float
getNodeValue na idx = arenaArrays na >>= \a -> readStyle a idx styleNodeValue
{-# INLINE setNodeValue #-}
setNodeValue :: NodeArena -> NodeIdx -> Float -> IO ()
setNodeValue na idx v = arenaArrays na >>= \a -> writeStyle a idx styleNodeValue v
{-# INLINE getNodeFontSize #-}
getNodeFontSize :: NodeArena -> NodeIdx -> IO Float
getNodeFontSize na idx = arenaArrays na >>= \a -> readStyle a idx styleFontSize
{-# INLINE setNodeFontSize #-}
setNodeFontSize :: NodeArena -> NodeIdx -> Float -> IO ()
setNodeFontSize na idx v = arenaArrays na >>= \a -> writeStyle a idx styleFontSize v
-- | Per-node font color (paint-only, kept out of @naArrTree@
-- where 'treeGridCols' holds the grid column count for containers).
{-# INLINE getNodeFontColor #-}
getNodeFontColor :: NodeArena -> NodeIdx -> IO (Maybe Color)
getNodeFontColor na idx = do
a <- arenaArrays na
val <- readPrimArray (naArrFontColor a) idx
if (val .&. 0x100000000) /= 0
then pure (Just (Color (fromIntegral (val .&. 0xFFFFFFFF))))
else pure Nothing
{-# INLINE setNodeFontColor #-}
setNodeFontColor :: NodeArena -> NodeIdx -> Maybe Color -> IO ()
setNodeFontColor na idx mCol = do
a <- arenaArrays na
let val = case mCol of
Nothing -> 0
Just (Color w) -> 0x100000000 .|. fromIntegral w
writePrimArray (naArrFontColor a) idx val
{-# INLINE getNodeScope #-}
getNodeScope :: NodeArena -> NodeIdx -> IO Int
getNodeScope na idx = arenaArrays na >>= \a -> readPrimArray (naArrScope a) idx
{-# INLINE getArenaScope #-}
getArenaScope :: NodeArena -> IO Int
getArenaScope na = readIORef (naScope na)
{-# INLINE setArenaScope #-}
setArenaScope :: NodeArena -> Int -> IO ()
setArenaScope na = writeIORef (naScope na)
{-# INLINE getScopeSignature #-}
getScopeSignature :: NodeArena -> IO Word64
getScopeSignature na = readIORef (naScopeSig na)
{-# INLINE getStyleIdx #-}
getStyleIdx :: NodeArena -> NodeIdx -> IO Int
getStyleIdx na idx = arenaArrays na >>= \a -> readTree a idx treeStyleIdx
{-# INLINE setStyleIdx #-}
setStyleIdx :: NodeArena -> NodeIdx -> Int -> IO ()
setStyleIdx na idx v = arenaArrays na >>= \a -> writeTree a idx treeStyleIdx v
-- | Get the snapshot buffers for a recursion depth, grown to hold at least
-- @needed@ entries. Buffers are reused across frames; nothing is allocated in
-- steady state once capacity is warm.
{-# NOINLINE ensureAxisSnapshot #-}
ensureAxisSnapshot :: NodeArena -> Int -> Int -> IO AxisSnapshot
ensureAxisSnapshot na depth needed = do
arr0 <- readIORef (naSnapLevels na)
let !d = max 0 depth
arr <- ensureSnapLevelsArr na arr0 (d + 1)
cap <- readIORef (naSnapCap na)
if needed <= cap
then getLevel arr d cap
else do
let !newCap = max needed (cap * 2)
!levels = sizeofMutableArray arr
forM_ [0 .. levels - 1] $ \i -> do
m <- readArray arr i
case m of
Nothing -> pure ()
Just (AxisSnapshot idx out) -> do
idx' <- growPrimArrayCopy idx cap newCap 0
out' <- growPrimArrayCopy out cap newCap 0
writeArray arr i (Just (AxisSnapshot idx' out'))
writeIORef (naSnapCap na) newCap
getLevel arr d newCap
where
getLevel arr d currentCap = do
m <- readArray arr d
case m of
Just s -> pure s
Nothing -> do
asIdx <- newPrimArray currentCap
asOut <- newPrimArray currentCap
let s = AxisSnapshot asIdx asOut
writeArray arr d (Just s)
pure s
-- | Grow the per-depth snapshot-level array to hold at least @need@ levels,
-- so nesting depth has no fixed limit.
ensureSnapLevelsArr :: NodeArena -> MutableArray RealWorld (Maybe AxisSnapshot) -> Int -> IO (MutableArray RealWorld (Maybe AxisSnapshot))
ensureSnapLevelsArr na arr need = do
let !sz = sizeofMutableArray arr
if need <= sz
then pure arr
else do
let !newSz = max need (sz * 2)
arr' <- newArray newSz Nothing
copyMutableArray arr' 0 arr 0 sz
writeIORef (naSnapLevels na) arr'
pure arr'
-- | Memoize @compute@ for node @idx@ at width @key@ in one of the arena's
-- per-frame memos. Widths within 0.25 px share an entry so near-identical
-- reflows still hit.
{-# INLINE memoizeWidth #-}
memoizeWidth :: NodeArena -> IORef WidthMemo -> NodeIdx -> Float -> IO (Float, Float) -> IO (Float, Float)
memoizeWidth na ref idx key compute = do
ft <- readIORef (naFrameTag na)
WidthMemo tags slots <- readIORef ref
tag <- readPrimArray tags idx
let !base = idx * memoStride
hit <-
if tag /= ft
then pure False
else do
k <- readPrimArray slots base
pure (abs (k - key) <= 0.25)
if hit
then (,) <$> readPrimArray slots (base + 1) <*> readPrimArray slots (base + 2)
else do
r@(x, y) <- compute
WidthMemo tags' slots' <- readIORef ref
writePrimArray tags' idx ft
writePrimArray slots' base key
writePrimArray slots' (base + 1) x
writePrimArray slots' (base + 2) y
pure r
-- | The flex scratch, grown to hold at least @needed@ entries.
{-# INLINE ensureScratchCapacity #-}
ensureScratchCapacity :: NodeArena -> Int -> IO FlexScratch
ensureScratchCapacity na needed = do
s <- readIORef (naScratch na)
if needed <= fsCap s then pure s else growScratch na s needed
{-# NOINLINE growScratch #-}
growScratch :: NodeArena -> FlexScratch -> Int -> IO FlexScratch
growScratch na s needed = do
let !cap = fsCap s
!newCap = max needed (cap * 2)
fsIdx <- growPrimArrayCopy (fsIdx s) cap newCap (-1)
fsW <- growPrimArrayCopy (fsW s) cap newCap 0
fsH <- growPrimArrayCopy (fsH s) cap newCap 0
fsOutW <- growPrimArrayCopy (fsOutW s) cap newCap 0
fsOutH <- growPrimArrayCopy (fsOutH s) cap newCap 0
let s' = FlexScratch {fsCap = newCap, ..}
writeIORef (naScratch na) s'
pure s'
{-# INLINE forNodes_ #-}
forNodes_ :: NodeArena -> (NodeIdx -> IO ()) -> IO ()
forNodes_ na f = do
n <- arenaCount na
let go !i
| i >= n = pure ()
| otherwise = f i >> go (i + 1)
go 0
{-# INLINE forChildNodes_ #-}
forChildNodes_ :: NodeArena -> NodeIdx -> (NodeIdx -> IO ()) -> IO ()
forChildNodes_ na parentIdx f = do
fc <- getFirstChild na parentIdx
let go !ci
| ci < 0 = pure ()
| otherwise = do
f ci
ns <- getNextSibling na ci
go ns
go fc
-- | Fold over a node's children in sibling order, skipping floating
-- (modal, window, popup) children, which are placed outside the flow.
{-# INLINE foldFlowChildrenM #-}
foldFlowChildrenM :: NodeArena -> NodeIdx -> (acc -> NodeIdx -> IO acc) -> acc -> IO acc
foldFlowChildrenM na parentIdx f z = do
fc <- getFirstChild na parentIdx
let go !ci !acc
| ci < 0 = pure acc
| otherwise = do
nt <- getNodeType na ci
ns <- getNextSibling na ci
if isFloatingNode nt
then go ns acc
else f acc ci >>= go ns
go fc z
{-# INLINE findNodeRevM #-}
findNodeRevM :: NodeArena -> (NodeIdx -> IO Bool) -> IO (Maybe NodeIdx)
findNodeRevM na p = do
n <- arenaCount na
let go !i
| i < 0 = pure Nothing
| otherwise = do
ok <- p i
if ok then pure (Just i) else go (i - 1)
go (n - 1)
{-# INLINE foldNodeRevM #-}
foldNodeRevM :: NodeArena -> (a -> NodeIdx -> IO a) -> a -> IO a
foldNodeRevM na f z = do
n <- arenaCount na
let go !i !acc
| i < 0 = pure acc
| otherwise = do
acc' <- f acc i
go (i - 1) acc'
go (n - 1) z
-- ---------------------------------------------------------------------------
-- Frame traversal helpers: forward node scans and child searches, shaped like
-- 'forNodes_' and 'findNodeRevM'.
-- ---------------------------------------------------------------------------
-- | First node, in arena order, satisfying the predicate.
{-# INLINE findNodeM #-}
findNodeM :: NodeArena -> (NodeIdx -> IO Bool) -> IO (Maybe NodeIdx)
findNodeM na p = do
n <- arenaCount na
let go !i
| i >= n = pure Nothing
| otherwise = do
ok <- p i
if ok then pure (Just i) else go (i + 1)
go 0
-- | Left fold over every node in arena order.
{-# INLINE foldNodesM #-}
foldNodesM :: NodeArena -> (a -> NodeIdx -> IO a) -> a -> IO a
foldNodesM na f z = do
n <- arenaCount na
let go !i !acc
| i >= n = pure acc
| otherwise = f acc i >>= go (i + 1)
go 0 z
-- | First direct child of @parentIdx@ satisfying the predicate.
{-# INLINE findChildM #-}
findChildM :: NodeArena -> NodeIdx -> (NodeIdx -> IO Bool) -> IO (Maybe NodeIdx)
findChildM na parentIdx p = do
fc <- getFirstChild na parentIdx
let go !ci
| ci < 0 = pure Nothing
| otherwise = do
ok <- p ci
if ok then pure (Just ci) else getNextSibling na ci >>= go
go fc