packages feed

monomer-1.4.0.0: src/Monomer/Widgets/Singles/TextField.hs

{-|
Module      : Monomer.Widgets.Singles.TextField
Copyright   : (c) 2018 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Input field for single line 'Text'. Allows setting the maximum number of
characters and a replacement character for password.

@
textField shortTextLens
@

With configuration options:

@
textField_ shortTextLens [maxLength 100, selectOnFocus_ False]
@

-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Singles.TextField (
  -- * Configuration
  TextFieldCfg,
  textFieldDisplayChar,
  -- * Constructors
  textField,
  textField_,
  textFieldV,
  textFieldV_,
  textFieldD_
) where

import Control.Applicative ((<|>))
import Control.Lens (ALens')
import Data.Default
import Data.Maybe
import Data.Text (Text)

import qualified Data.Text as T

import Monomer.Core
import Monomer.Core.Combinators
import Monomer.Widgets.Singles.Base.InputField

import qualified Monomer.Lens as L

{-|
Configuration options for textField:

- 'validInput': field indicating if the current input is valid. Useful to show
  warnings in the UI, or disable buttons if needed.
- 'resizeOnChange': Whether input causes ResizeWidgets requests.
- 'selectOnFocus': Whether all input should be selected when focus is received.
- 'readOnly': Whether to prevent the user changing the input text.
- 'maxLength': the maximum length of input text.
- 'textFieldDisplayChar': the character that will be displayed as replacement of
  the real text. Useful for password fields.
- 'onFocus': event to raise when focus is received.
- 'onFocusReq': 'WidgetRequest' to generate when focus is received.
- 'onBlur': event to raise when focus is lost.
- 'onBlurReq': 'WidgetRequest' to generate when focus is lost.
- 'onChange': event to raise when the value changes.
- 'onChangeReq': 'WidgetRequest' to generate when the value changes.
-}
data TextFieldCfg s e = TextFieldCfg {
  _tfcCaretWidth :: Maybe Double,
  _tfcCaretMs :: Maybe Millisecond,
  _tfcDisplayChar :: Maybe Char,
  _tfcPlaceholder :: Maybe Text,
  _tfcValid :: Maybe (WidgetData s Bool),
  _tfcValidV :: [Bool -> e],
  _tfcMaxLength :: Maybe Int,
  _tfcResizeOnChange :: Maybe Bool,
  _tfcSelectOnFocus :: Maybe Bool,
  _tfcReadOnly :: Maybe Bool,
  _tfcOnFocusReq :: [Path -> WidgetRequest s e],
  _tfcOnBlurReq :: [Path -> WidgetRequest s e],
  _tfcOnChangeReq :: [Text -> WidgetRequest s e]
}

instance Default (TextFieldCfg s e) where
  def = TextFieldCfg {
    _tfcCaretWidth = Nothing,
    _tfcCaretMs = Nothing,
    _tfcDisplayChar = Nothing,
    _tfcPlaceholder = Nothing,
    _tfcValid = Nothing,
    _tfcValidV = [],
    _tfcMaxLength = Nothing,
    _tfcResizeOnChange = Nothing,
    _tfcSelectOnFocus = Nothing,
    _tfcReadOnly = Nothing,
    _tfcOnFocusReq = [],
    _tfcOnBlurReq = [],
    _tfcOnChangeReq = []
  }

instance Semigroup (TextFieldCfg s e) where
  (<>) t1 t2 = TextFieldCfg {
    _tfcCaretWidth = _tfcCaretWidth t2 <|> _tfcCaretWidth t1,
    _tfcCaretMs = _tfcCaretMs t2 <|> _tfcCaretMs t1,
    _tfcDisplayChar = _tfcDisplayChar t2 <|> _tfcDisplayChar t1,
    _tfcPlaceholder = _tfcPlaceholder t2 <|> _tfcPlaceholder t1,
    _tfcValid = _tfcValid t2 <|> _tfcValid t1,
    _tfcValidV = _tfcValidV t1 <> _tfcValidV t2,
    _tfcMaxLength = _tfcMaxLength t2 <|> _tfcMaxLength t1,
    _tfcResizeOnChange = _tfcResizeOnChange t2 <|> _tfcResizeOnChange t1,
    _tfcSelectOnFocus = _tfcSelectOnFocus t2 <|> _tfcSelectOnFocus t1,
    _tfcReadOnly = _tfcReadOnly t2 <|> _tfcReadOnly t1,
    _tfcOnFocusReq = _tfcOnFocusReq t1 <> _tfcOnFocusReq t2,
    _tfcOnBlurReq = _tfcOnBlurReq t1 <> _tfcOnBlurReq t2,
    _tfcOnChangeReq = _tfcOnChangeReq t1 <> _tfcOnChangeReq t2
  }

instance Monoid (TextFieldCfg s e) where
  mempty = def

instance CmbCaretWidth (TextFieldCfg s e) Double where
  caretWidth w = def {
    _tfcCaretWidth = Just w
  }

instance CmbCaretMs (TextFieldCfg s e) Millisecond where
  caretMs ms = def {
    _tfcCaretMs = Just ms
  }

instance CmbPlaceholder (TextFieldCfg s e) Text where
  placeholder value = def {
    _tfcPlaceholder = Just value
  }

instance CmbValidInput (TextFieldCfg s e) s where
  validInput field = def {
    _tfcValid = Just (WidgetLens field)
  }

instance CmbValidInputV (TextFieldCfg s e) e where
  validInputV fn = def {
    _tfcValidV = [fn]
  }

instance CmbResizeOnChange (TextFieldCfg s e) where
  resizeOnChange_ resize = def {
    _tfcResizeOnChange = Just resize
  }

instance CmbSelectOnFocus (TextFieldCfg s e) where
  selectOnFocus_ sel = def {
    _tfcSelectOnFocus = Just sel
  }

instance CmbReadOnly (TextFieldCfg s e) where
  readOnly_ ro = def {
    _tfcReadOnly = Just ro
  }

instance CmbMaxLength (TextFieldCfg s e) where
  maxLength len = def {
    _tfcMaxLength = Just len
  }

instance WidgetEvent e => CmbOnFocus (TextFieldCfg s e) e Path where
  onFocus fn = def {
    _tfcOnFocusReq = [RaiseEvent . fn]
  }

instance CmbOnFocusReq (TextFieldCfg s e) s e Path where
  onFocusReq req = def {
    _tfcOnFocusReq = [req]
  }

instance WidgetEvent e => CmbOnBlur (TextFieldCfg s e) e Path where
  onBlur fn = def {
    _tfcOnBlurReq = [RaiseEvent . fn]
  }

instance CmbOnBlurReq (TextFieldCfg s e) s e Path where
  onBlurReq req = def {
    _tfcOnBlurReq = [req]
  }

instance WidgetEvent e => CmbOnChange (TextFieldCfg s e) Text e where
  onChange fn = def {
    _tfcOnChangeReq = [RaiseEvent . fn]
  }

instance CmbOnChangeReq (TextFieldCfg s e) s e Text where
  onChangeReq req = def {
    _tfcOnChangeReq = [req]
  }

-- | Replacement character to show instead of real text. Useful for passwords.
textFieldDisplayChar :: Char -> TextFieldCfg s e
textFieldDisplayChar char = def {
    _tfcDisplayChar = Just char
  }

-- | Creates a text field using the given lens.
textField :: WidgetEvent e => ALens' s Text -> WidgetNode s e
textField field = textField_ field def

-- | Creates a text field using the given lens. Accepts config.
textField_
  :: WidgetEvent e => ALens' s Text -> [TextFieldCfg s e] -> WidgetNode s e
textField_ field configs = textFieldD_ (WidgetLens field) configs

-- | Creates a text field using the given value and 'onChange' event handler.
textFieldV :: WidgetEvent e => Text -> (Text -> e) -> WidgetNode s e
textFieldV value handler = textFieldV_ value handler def

-- | Creates a text field using the given value and 'onChange' event handler.
--   Accepts config.
textFieldV_
  :: WidgetEvent e => Text -> (Text -> e) -> [TextFieldCfg s e] -> WidgetNode s e
textFieldV_ value handler configs = textFieldD_ widgetData newConfig where
  widgetData = WidgetValue value
  newConfig = onChange handler : configs

-- | Creates a text field providing a 'WidgetData' instance and config.
textFieldD_
  :: WidgetEvent e => WidgetData s Text -> [TextFieldCfg s e] -> WidgetNode s e
textFieldD_ widgetData configs = inputField where
  config = mconcat configs
  fromText = textToText (_tfcMaxLength config)
  inputConfig = InputFieldCfg {
    _ifcPlaceholder = _tfcPlaceholder config,
    _ifcInitialValue = "",
    _ifcValue = widgetData,
    _ifcValid = _tfcValid config,
    _ifcValidV = _tfcValidV config,
    _ifcFromText = fromText,
    _ifcToText = id,
    _ifcAcceptInput = acceptInput (_tfcMaxLength config),
    _ifcIsValidInput = acceptInput (_tfcMaxLength config),
    _ifcDefCursorEnd = True,
    _ifcDefWidth = 100,
    _ifcCaretWidth = _tfcCaretWidth config,
    _ifcCaretMs = _tfcCaretMs config,
    _ifcDisplayChar = _tfcDisplayChar config,
    _ifcResizeOnChange = fromMaybe False (_tfcResizeOnChange config),
    _ifcSelectOnFocus = fromMaybe False (_tfcSelectOnFocus config),
    _ifcReadOnly = fromMaybe False (_tfcReadOnly config),
    _ifcStyle = Just L.textFieldStyle,
    _ifcWheelHandler = Nothing,
    _ifcDragHandler = Nothing,
    _ifcDragCursor = Nothing,
    _ifcOnFocusReq = _tfcOnFocusReq config,
    _ifcOnBlurReq = _tfcOnBlurReq config,
    _ifcOnChangeReq = _tfcOnChangeReq config
  }
  inputField = inputField_ "textField" inputConfig

textToText :: Maybe Int -> Text -> Maybe Text
textToText Nothing text = Just text
textToText (Just len) text
  | T.length text <= len = Just text
  | otherwise = Nothing

acceptInput :: Maybe Int -> Text -> Bool
acceptInput Nothing _ = True
acceptInput (Just len) text = T.length text <= len