packages feed

nano-ui-0.1.0.0: lib/NanoUI/Widgets/Radio.hs

{-# LANGUAGE OverloadedStrings #-}

module NanoUI.Widgets.Radio
  ( radio
  , radio'
  , boundedRadio
  , boundedRadio'
  , enumRadio
  , enumRadio'
  )
where

import Control.Monad (foldM)
import Data.Foldable (toList)
import Data.Hashable (hash)
import Data.Text (Text)
import Data.Text qualified as T
import Effectful (Eff, type (:>))
import NanoUI.Context (adoptStoreInt, intKey, recordStoreInt, registerFocusable, writeStoreInt)
import NanoUI.Layout.Arena (NodeType (..))
import NanoUI.Monad (Ui, askContext, nextId, uiIO, withKey)
import NanoUI.Style (Layout, defaultLayout, fillW, gap, tight)
import NanoUI.Types (clamp)
import NanoUI.Widgets.Behavior (KeyNav (..), useKeyNav)
import NanoUI.Widgets.Combinators (selectableItem, withBoundedIndex)
import NanoUI.Widgets.Layout (column')
import NanoUI.Widgets.Node
  ( Response (..)
  , setChanged
  , tagContainer
  )

radioLay :: Layout
radioLay = tight (fillW defaultLayout)

radioGroupLay :: Layout
radioGroupLay = tight (gap 4 (fillW defaultLayout))

radioSalt :: Int
radioSalt = hash ("radio" :: Text)

-- | A column of radio buttons over @options@ in fold order. Pass the selected
-- index; the result is the index after this frame's click or arrow keys.
{-# INLINE radio #-}
radio :: (Foldable f, Ui :> es) => f Text -> Int -> Eff es Int
radio options index = snd <$> radio' options index

radio' ::
  (Foldable f, Ui :> es) => f Text -> Int -> Eff es (Response, Int)
radio' options index =
  withKey radioSalt $ do
    gid <- nextId
    ctx <- askContext
    let
      opts = case toList options of
        [] -> [""]
        xs -> xs
      !len = length opts
      !key = intKey gid
    stored <- uiIO $ adoptStoreInt ctx gid key (clamp 0 (len - 1) index)
    let !sel = clamp 0 (len - 1) stored
    uiIO $ registerFocusable ctx gid
    nav <- useKeyNav gid
    let
      !navDelta =
        (if knDown nav || knRight nav then 1 else 0 :: Int)
          - (if knUp nav || knLeft nav then 1 else 0)
      !selNav = if navDelta == 0 then sel else clamp 0 (len - 1) (sel + navDelta)
    column' radioGroupLay $ do
      tagContainer gid
      (combinedResp, clickedIdx) <- addRadioOptions selNav opts
      let !finalSel = if clickedIdx >= 0 then clickedIdx else selNav
      uiIO $ do
        writeStoreInt ctx gid key finalSel
        recordStoreInt ctx key finalSel
      -- Compare with the caller's index, as 'NanoUI.Widgets.Select' does, so a
      -- selection stored between frames still reports a change.
      pure (setChanged (finalSel /= clamp 0 (len - 1) index) combinedResp, finalSel)

-- Use the ordinary widget path for every option, including singleton groups.
-- It owns IDs, node construction, and scroll-aware interaction geometry.
addRadioOptions :: Ui :> es => Int -> [Text] -> Eff es (Response, Int)
addRadioOptions sel opts = foldM addOption (mempty, -1) (zip [0 ..] opts)
 where
  addOption (!acc, !clickedIdx) (i, txt) = do
    r <- selectableItem NodeRadio txt (sel == i) radioLay i
    let
      clickedIdx' = if rawRespClicked r && clickedIdx < 0 then i else clickedIdx
    pure (acc <> r, clickedIdx')

-- | Radio buttons for every value of a bounded enum, labelled by @encode@.
{-# INLINE boundedRadio #-}
boundedRadio :: (Bounded a, Enum a, Ui :> es) => (a -> Text) -> a -> Eff es a
boundedRadio encode value = snd <$> boundedRadio' encode value

boundedRadio' :: (Bounded a, Enum a, Ui :> es) => (a -> Text) -> a -> Eff es (Response, a)
boundedRadio' encode value = withBoundedIndex encode value radio'

-- | 'boundedRadio' labelled with 'show'.
{-# INLINE enumRadio #-}
enumRadio :: (Bounded a, Enum a, Show a, Ui :> es) => a -> Eff es a
enumRadio = boundedRadio (T.pack . show)

enumRadio' :: (Bounded a, Enum a, Show a, Ui :> es) => a -> Eff es (Response, a)
enumRadio' = boundedRadio' (T.pack . show)