packages feed

brillo-2.0.0: Brillo/Internals/TinyFileDialogs.chs

{-# LANGUAGE LambdaCase #-}

module Brillo.Internals.TinyFileDialogs (
  -- * Functions
  beep,
  notifyPopup,
  messageBox,
  inputBox,
  saveFileDialog,
  openFileDialog,
  selectFolderDialog,
  colorChooser,
  -- * Message box options
  IconType(..),
  MessageBox,
  OK(..),
  OKCancel(..),
  YesNo(..),
  YesNoCancel(..),
)
where

import Prelude (
  Applicative (pure),
  Bool (..),
  Bounded,
  Enum,
  Eq ((==)),
  fromIntegral,
  Functor (fmap),
  id,
  Int,
  IO,
  maxBound,
  Maybe (Just, Nothing),
  minBound,
  Ord,
  Read,
  return,
  Semigroup ((<>)),
  show,
  Show,
  (.),
  (<>),
  (>>),
  (>>=),
  ($),
 )

import Data.List (lookup)
import Data.Char (toLower)
import Data.Text qualified as T
import Foreign (Ptr, Word8, nullPtr, peekArray, withArray, withArrayLen, withMany)
import Foreign.C (CInt, CString, CUChar)
import System.Exit (die)

#ifdef WINDOWS
import Data.ByteString qualified as B
import Data.Text.Encoding qualified as TE
#else
import Foreign.C (peekCString, withCString)
#endif

#include "tinyfiledialogs.h"

{#context prefix = "tinyfd_" #}

withCText :: T.Text -> (CString -> IO a) -> IO a
#ifdef WINDOWS
withCText = B.useAsCString . TE.encodeUtf8
#else
withCText = withCString . T.unpack
#endif

withCShowLower :: (Show a) => a -> (CString -> IO b) -> IO b
withCShowLower = withCText . T.pack . fmap toLower . show

withCMaybeText :: Maybe T.Text -> (CString -> IO a) -> IO a
withCMaybeText mt f = case mt of
  Nothing -> f nullPtr
  Just t  -> withCText t f

peekMaybeText :: CString -> IO (Maybe T.Text)
peekMaybeText cstr = if cstr == nullPtr
  then pure Nothing
#ifdef WINDOWS
  else fmap (Just . TE.decodeUtf8) $ B.packCString cstr
#else
  else fmap (Just . T.pack) $ peekCString cstr
#endif

peekMaybeTextMultiple :: CString -> IO (Maybe [T.Text])
peekMaybeTextMultiple = fmap (fmap $ T.splitOn (T.singleton '|')) . peekMaybeText

withCTexts :: [T.Text] -> ((CInt, Ptr CString) -> IO a) -> IO a
withCTexts ts f = withMany withCText ts $ \ptrs ->
  withArrayLen ptrs $ \len ptr -> f (fromIntegral len, ptr)

class (Enum a, Bounded a) => MessageBox a where
  messageBoxType :: a -> T.Text
  messageBoxValue :: a -> Int

data IconType = Info | Warning | Error | Question
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

data OK = OK
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

instance MessageBox OK where
  messageBoxType _ = T.pack "ok"
  messageBoxValue OK = 1

data OKCancel = OC_OK | OC_Cancel
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

instance MessageBox OKCancel where
  messageBoxType _ = T.pack "okcancel"
  messageBoxValue OC_Cancel = 0
  messageBoxValue OC_OK     = 1

data YesNo = YN_Yes | YN_No
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

instance MessageBox YesNo where
  messageBoxType _ = T.pack "yesno"
  messageBoxValue YN_No  = 0
  messageBoxValue YN_Yes = 1

data YesNoCancel = YNC_Yes | YNC_No | YNC_Cancel
  deriving (Eq, Ord, Show, Read, Enum, Bounded)

instance MessageBox YesNoCancel where
  messageBoxType _ = T.pack "yesnocancel"
  messageBoxValue YNC_Cancel = 0
  messageBoxValue YNC_Yes    = 1
  messageBoxValue YNC_No     = 2

{#fun messageBox as c_messageBox
  { withCText*      `T.Text'     -- ^ title
  , withCText*      `T.Text'     -- ^ message, may contain @\\n@ and @\\t@
  , withCText*      `T.Text'     -- ^ @"ok" "okcancel" "yesno" "yesnocancel"@
  , withCShowLower* `IconType'   -- ^ 'Info', 'Warning', 'Error', 'Question'
  ,                 `Int'        -- ^ default button: 0 for cancel/no, 1 for ok/yes, 2 for no in yesnocancel
  } -> `Int' -- ^ 0 for cancel/no, 1 for ok/yes, 2 for no in yesnocancel
#}

{#fun notifyPopup
  { withCText*      `T.Text'   -- ^ title
  , withCText*      `T.Text'   -- ^ message, may contain @\\n@ and @\\t@
  , withCShowLower* `IconType' -- ^ 'Info', 'Warning', 'Error'
  } -> `()'
#}

{#fun beep {} -> `()' #}

messageBox
  :: (MessageBox a)
  => T.Text -- ^ title
  -> T.Text -- ^ message, may contain @\\n@ and @\\t@
  -> IconType -- ^ 'Info', 'Warning', 'Error', 'Question'
  -> a -- ^ default button
  -> IO a
messageBox ttl msg icon dflt = do
  n <- c_messageBox ttl msg (messageBoxType dflt) icon (messageBoxValue dflt)
  case lookup n [ (messageBoxValue x, x) | x <- [minBound .. maxBound] ] of
    Just x  -> pure x
    Nothing -> die $ "TinyFileDialogs.messageBox: "
      <> "internal error; unrecognized return value " <> show n

{#fun inputBox
  { withCText*      `T.Text'       -- ^ title
  , withCText*      `T.Text'       -- ^ message, may NOT contain @\\n@ and @\\t@ on windows
  , withCMaybeText* `Maybe T.Text' -- ^ default input, if 'Nothing' it's a passwordBox
  } -> `Maybe T.Text' peekMaybeText* -- ^ returns 'Nothing' on cancel
#}

{#fun saveFileDialog
  { withCText*  `T.Text'    -- ^ title
  , withCText*  `T.Text'    -- ^ default path and file
  , withCTexts* `[T.Text]'& -- ^ filter patterns, @["*.jpg","*.png"]@
  , withCText*  `T.Text'    -- ^ single filter description, @"text files"@
  } -> `Maybe T.Text' peekMaybeText* -- ^ returns 'Nothing' on cancel
#}

{#fun openFileDialog
  { withCText*  `T.Text'    -- ^ title
  , withCText*  `T.Text'    -- ^ default path and file
  , withCTexts* `[T.Text]'& -- ^ filter patterns, @["*.jpg","*.png"]@
  , withCText*  `T.Text'    -- ^ single filter description, @"text files"@
  ,             `Bool'      -- ^ allow multiple selects
  } -> `Maybe [T.Text]' peekMaybeTextMultiple* -- ^ returns 'Nothing' on cancel
#}

{#fun selectFolderDialog
  { withCText* `T.Text' -- ^ title
  , withCText* `T.Text' -- ^ default path
  } -> `Maybe T.Text' peekMaybeText* -- ^ returns 'Nothing' on cancel
#}

{#fun colorChooser as c_colorChooser
  { withCText*      `T.Text'
  , withCMaybeText* `Maybe T.Text'
  , id              `Ptr CUChar'
  , id              `Ptr CUChar'
  } -> `Maybe T.Text' peekMaybeText* -- ^ returns 'Nothing' on cancel
#}

withColor :: (Word8, Word8, Word8) -> (Ptr CUChar -> IO a) -> IO a
withColor (r, g, b) = withArray $ fmap fromIntegral [r, g, b]

colorChooser
  :: T.Text                           -- ^ title
  -> (Word8, Word8, Word8)            -- ^ default RGB color
  -> IO (Maybe (Word8, Word8, Word8)) -- ^ returns 'Nothing' on cancel
colorChooser title color = withColor color $ \ptr -> do
  res <- c_colorChooser title Nothing ptr ptr
  case res of
    Nothing -> pure Nothing
    Just _  -> fmap
      ((\case
        [r, g, b] -> Just (r, g, b)
        _        -> Nothing
        ) . fmap fromIntegral)
      (peekArray 3 ptr)