packages feed

gi-gtk-declarative-0.6.3: test/GI/Gtk/Declarative/TestWidget.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}

module GI.Gtk.Declarative.TestWidget where

import           Control.Applicative
import           Control.Monad.Except
import           Data.Text                      ( Text )
import           Data.Traversable               ( for )
import           Data.Vector                    ( Vector )
import qualified Data.Vector                   as Vector
import           Data.Void
import qualified GI.Gtk                        as Gtk
import           GI.Gtk.Declarative
import           GI.Gtk.Declarative.EventSource
import           Hedgehog                hiding ( label )
import qualified Hedgehog.Gen                  as Gen
import qualified Hedgehog.Range                as Range
import           Prelude

-- | GTK widgets cannot (in any practical, generic sense) be compared and shown
-- in tests, so we represent widgets in property tests using this data
-- structure. We convert between this representation, declarative widgets, and
-- instantiated GTK widgets.
data TestWidget
  = TestButton Text (Maybe Bool)
  | TestCustomWidget (Maybe Text)
  | TestScrolledWindow (Maybe Gtk.PolicyType) TestWidget
  | TestBox (Maybe Gtk.Orientation) [TestBoxChild]
  deriving (Eq, Show)

data TestBoxChild = TestBoxChild BoxChildProperties TestWidget
  deriving (Eq, Show)

isNested :: TestWidget -> Bool
isNested = \case
  TestButton{}                  -> False
  TestCustomWidget{}            -> False
  TestScrolledWindow _ _        -> True
  TestBox            _ children -> not (null children)

class HasGtkDefaults a where
  setDefaults :: a -> a

instance HasGtkDefaults TestWidget where
  setDefaults = \case
    TestButton label useUnderline ->
      TestButton label (useUnderline <|> Just False)
    TestCustomWidget fontName -> TestCustomWidget (fontName <|> Just "Sans 12")
    TestScrolledWindow policy child -> TestScrolledWindow
      (policy <|> Just Gtk.PolicyTypeAutomatic)
      (setDefaults child)
    TestBox orientation children -> TestBox
      (orientation <|> Just Gtk.OrientationHorizontal)
      (map setDefaults children)

instance HasGtkDefaults TestBoxChild where
  setDefaults = \case
    TestBoxChild props child -> TestBoxChild props (setDefaults child)

onlyJusts :: Vector (Maybe a) -> Vector a
onlyJusts = Vector.concatMap (maybe Vector.empty Vector.singleton)

toTestWidget :: TestWidget -> Widget Void
toTestWidget = \case
  TestCustomWidget fontName -> Widget (CustomWidget { .. })
   where
    customParams     = ()
    customAttributes = case fontName of
      Just t  -> [#fontName := t]
      Nothing -> []
    customWidget = Gtk.FontButton
    customCreate () = do
      btn <- Gtk.new Gtk.FontButton []
      return (btn, ())
    customPatch :: () -> () -> () -> CustomPatch Gtk.FontButton ()
    customPatch _ () () = CustomKeep
    customSubscribe
      :: () -> () -> Gtk.FontButton -> (Void -> IO ()) -> IO Subscription
    customSubscribe () () _lbl _cb = do
      return (fromCancellation (pure ()))
  TestButton label useUnderline -> widget
    Gtk.Button
    (onlyJusts [Just (#label := label), (#useUnderline :=) <$> useUnderline])
  TestScrolledWindow policy child -> bin
    Gtk.ScrolledWindow
    (onlyJusts [(#vscrollbarPolicy :=) <$> policy])
    (toTestWidget child)
  TestBox orientation children -> container
    Gtk.Box
    (onlyJusts [(#orientation :=) <$> orientation])
    (Vector.map
      (\(TestBoxChild props child) -> BoxChild props (toTestWidget child))
      (Vector.fromList children)
    )

fromGtkWidget :: (MonadIO m) => Gtk.Widget -> m (Either Text TestWidget)
fromGtkWidget = runExceptT . go
 where
  go :: (MonadIO m) => Gtk.Widget -> ExceptT Text m TestWidget
  go w = do
    name <- #getName w
    case name of
      "GtkButton" -> withCast
        w
        Gtk.Button
        (\btn ->
          TestButton
            <$> Gtk.get btn #label
            <*> (Just <$> Gtk.get btn #useUnderline)
        )
      "GtkFontButton" -> withCast
        w
        Gtk.FontButton
        (\btn -> TestCustomWidget . Just <$> Gtk.get btn #fontName)
      "GtkScrolledWindow" -> withCast w Gtk.ScrolledWindow $ \win -> do
        w' <-
          #getChild win
            >>= maybe (throwError "No viewport in scrolled window") pure
        vscrollbarPolicy <- Just <$> Gtk.get win #vscrollbarPolicy
        withCast w' Gtk.Viewport $ \viewport -> do
          child <-
            #getChild viewport
              >>= maybe (throwError "No child in scrolled window") pure
          TestScrolledWindow vscrollbarPolicy <$> go child
      "GtkBox" -> withCast w Gtk.Box $ \box -> do
        childGtkWidgets <- #getChildren box
        boxChildProps   <- for childGtkWidgets $ \childGtkWidget -> do
          (expand, fill, padding, _) <- #queryChildPacking box childGtkWidget
          pure (BoxChildProperties expand fill padding)
        childWidgets <- traverse go childGtkWidgets
        orientation  <- Just <$> Gtk.get box #orientation
        pure
          (TestBox orientation (zipWith TestBoxChild boxChildProps childWidgets)
          )
      _ -> throwError ("Unsupported TestWidget: " <> name)
  withCast
    :: (MonadIO m, Gtk.GObject w, Gtk.GObject w')
    => w
    -> (Gtk.ManagedPtr w' -> w')
    -> (w' -> ExceptT Text m a)
    -> ExceptT Text m a
  withCast w ctor f = liftIO (Gtk.castTo ctor w) >>= \case
    Just w' -> f w'
    Nothing -> throwError "Failed to cast widget"

-- * Generators

genTestWidget :: Gen TestWidget
genTestWidget = Gen.frequency
  (map (3, ) leaves <> pure
    ( 2
    , Gen.recursive
      Gen.choice
      leaves
      (  subwidgets genTestBoxFrom
      <> [ Gen.subtermM
             genTestWidget
             (\c -> TestScrolledWindow <$> Gen.maybe genPolicyType <*> pure c)
         ]
      )
    )
  )
 where
  leaves = [genCustomWidget, genButton]
  -- In lack of `subtermN` (https://github.com/hedgehogqa/haskell-hedgehog/issues/119), we use this terrible hack:
  subwidgets :: ([TestWidget] -> Gen TestWidget) -> [Gen TestWidget]
  subwidgets f =
    [ f []
    , Gen.subtermM genTestWidget (\w -> f [w])
    , Gen.subtermM2 genTestWidget genTestWidget (\w1 w2 -> f [w1, w2])
    , Gen.subtermM3 genTestWidget
                    genTestWidget
                    genTestWidget
                    (\w1 w2 w3 -> f [w1, w2, w3])
    ]
  genTestBoxFrom ws = do
    children <- for ws $ \w -> do
      props <- genBoxChildProperties
      pure (TestBoxChild props w)
    o <- Gen.maybe genOrientation
    pure (TestBox o children)

genOrientation :: Gen Gtk.Orientation
genOrientation =
  Gen.choice [pure Gtk.OrientationVertical, pure Gtk.OrientationHorizontal]

genPolicyType :: Gen Gtk.PolicyType
genPolicyType = Gen.choice
  (map
    pure
    [ Gtk.PolicyTypeAlways
    , Gtk.PolicyTypeAutomatic
    , Gtk.PolicyTypeExternal
    , Gtk.PolicyTypeNever
    ]
  )

genBoxChildProperties :: Gen BoxChildProperties
genBoxChildProperties =
  BoxChildProperties <$> Gen.bool <*> Gen.bool <*> Gen.word32
    (Range.linear 0 10)

genCustomWidget :: Gen TestWidget
genCustomWidget = do
  TestCustomWidget <$> Gen.maybe (Gen.choice [pure "Sans 10"])

genButton :: Gen TestWidget
genButton = do
  TestButton <$> Gen.text (Range.linear 0 10) Gen.unicode <*> Gen.maybe Gen.bool