packages feed

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

-- | Commands run on a text field from outside its frame: an app's Edit menu,
-- a toolbar button, the field's own context menu.
module NanoUI.Widgets.TextField
  ( runTextCommand
  , textCanUndo
  , textCanRedo
  , applyTextFieldCommand
  , textFieldMode
  , textFieldHistory
  ) where

import Data.Dynamic (fromDynamic)
import Data.IORef (writeIORef)
import Data.IntMap.Strict qualified as IM
import Data.Text (Text)
import Effectful (Eff, type (:>))
import NanoUI.Context (Context (..), WidgetStore (..), getStore, intKey, setTextInputMenu)
import NanoUI.Frame.Hit (findNodeByWidgetId)
import NanoUI.Id (WidgetId)
import NanoUI.Layout.Arena (NodeType (..), getNodeType, getStyleIdx)
import NanoUI.Monad (Ui, askContext, uiIO)
import NanoUI.Store (slotKey, Slot (..))
import NanoUI.Widgets.TextArea (applyTextAreaCommand)
import NanoUI.Widgets.TextEditor
  ( EditHistory
  , EditorMode (..)
  , TextCommand
  , canRedo
  , canUndo
  , editorModeFromCode
  , emptyHistory
  , multiLineMode
  )
import NanoUI.Widgets.TextInput (applyTextInputCommand, textInputMode)

-- | Run a command on the text field (text input, search field, text area)
-- with this id, as if its keys were pressed: @runTextCommand (respId resp)
-- Undo@. The field takes keyboard focus, and its next frame returns the
-- changed text and a 'NanoUI.respChanged' pulse. An id that is not a text
-- field is ignored.
runTextCommand :: Ui :> es => WidgetId -> TextCommand -> Eff es ()
runTextCommand wid cmd = do
  ctx <- askContext
  uiIO (applyTextFieldCommand ctx wid cmd)

-- | Whether 'NanoUI.Widgets.TextCommand.Undo' would change the field, for
-- enabling a menu item.
textCanUndo :: Ui :> es => WidgetId -> Eff es Bool
textCanUndo wid = do
  ctx <- askContext
  uiIO (canUndo <$> textFieldHistory ctx wid)

-- | Whether 'NanoUI.Widgets.TextCommand.Redo' would change the field, for
-- enabling a menu item.
textCanRedo :: Ui :> es => WidgetId -> Eff es Bool
textCanRedo wid = do
  ctx <- askContext
  uiIO (canRedo <$> textFieldHistory ctx wid)

-- | Run a command on the field with this id and focus it: the command comes
-- from a menu or button that may not be over the field, and the caret,
-- selection highlight and next keystroke belong to the field it edited.
applyTextFieldCommand :: Context -> WidgetId -> TextCommand -> IO ()
applyTextFieldCommand ctx wid cmd =
  textFieldMode ctx wid >>= \case
    Just mode -> do
      if modeMultiLine mode
        then applyTextAreaCommand ctx wid cmd
        else applyTextInputCommand ctx wid mode cmd
      writeIORef (ctxFocusId ctx) wid
      setTextInputMenu ctx Nothing
    Nothing -> pure ()

-- | How the field with this id edits: from its node when it has one this
-- frame, or from what it recorded the last time it was declared.
textFieldMode :: Context -> WidgetId -> IO (Maybe EditorMode)
textFieldMode ctx wid =
  findNodeByWidgetId ctx wid >>= \case
    Just idx ->
      getNodeType (ctxNodeArena ctx) idx >>= \case
        NodeTextInput -> Just . textInputMode <$> getStyleIdx (ctxNodeArena ctx) idx
        NodeTextArea -> pure (Just multiLineMode)
        _ -> pure Nothing
    Nothing -> do
      store <- getStore ctx
      pure (IM.lookup (slotKey SlotTextMode (intKey wid)) (storeInt store) >>= editorModeFromCode)

-- | The undo history of the field with this id, empty when it has none.
textFieldHistory :: Context -> WidgetId -> IO EditHistory
textFieldHistory ctx wid = do
  store <- getStore ctx
  let key = intKey wid
      stored = IM.lookup (slotKey SlotTextHistory key) (storeDyn store)
      text = IM.findWithDefault "" key (storeText store)
  pure $ case stored >>= fromDynamic of
    Just (recorded, h) | recorded == (text :: Text) -> h
    _ -> emptyHistory