{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE ImpredicativeTypes #-} ----------------------------------------------------------------------------- -- | -- Module : Sindre.Widgets -- License : MIT-style (see LICENSE) -- -- Stability : provisional -- Portability : portable -- -- Portable Sindre gadgets and helper functions that can be used by -- any backend. -- ----------------------------------------------------------------------------- module Sindre.Widgets ( mkHorizontally , mkVertically , changeField , changeField_ , changingField , Match(..) , match , filterMatches , sortMatches ) where import Sindre.Sindre import Sindre.Compiler import Sindre.Runtime import Control.Monad.Error import Control.Monad.State import Control.Applicative import Data.List import Data.Maybe import qualified Data.Map as M import qualified Data.Text as T data Oriented = Oriented { mergeSpace :: [SpaceNeed] -> SpaceNeed , splitSpace :: Rectangle -> [SpaceNeed] -> [Rectangle] , children :: [WidgetRef] } sumPrim :: [DimNeed] -> DimNeed sumPrim [] = Min 0 sumPrim (d:ds) = foldl f d ds where f (Min x) (Min y) = Min (x+y) f (Min x) (Max y) = Max (x+y) f (Min x) (Exact y) = Min (x+y) f (Max x) (Max y) = Max (x+y) f (Max x) (Exact y) = Max (x+y) f (Exact x) (Exact y) = Exact (x+y) f _ Unlimited = Unlimited f x y = f y x sumSec :: [DimNeed] -> DimNeed sumSec [] = Min 0 sumSec (d:ds) = foldl f d ds where f (Min x) (Min y) = Min $ max x y f (Min x) (Max y) | x < y = Max y f (Min x) (Max _) = Max x f (Min _) (Exact y) = Exact y f (Max x) (Max y) = Max $ max x y f (Max _) (Exact y) = Exact y f (Max x) Unlimited = Max x f (Exact x) (Exact y) = Exact $ max x y f (Exact x) Unlimited = Exact x f _ Unlimited = Unlimited f x y = f y x layouting :: MonadBackend m => (forall a. ((a, a) -> a)) -> Constructor m layouting f _ cs = return $ newWidget (Oriented merge split (map snd cs)) M.empty [] (const $ return ()) composeI drawI where merge rects = ( f (sumPrim, sumSec) $ map fst rects , f (sumSec, sumPrim) $ map snd rects ) split r = f (splitVert, splitHoriz) r . map f composeI = do chlds <- gets children gets mergeSpace <*> mapM compose chlds drawI r = do chlds <- gets children rects <- gets splitSpace <*> pure r <*> mapM compose chlds concat <$> zipWithM draw (reverse chlds) (Just <$> reverse rects) -- | A widget that arranges its children in a horizontal row. mkHorizontally :: MonadBackend m => Constructor m mkHorizontally = layouting fst -- | A widget that arranges its children in a vertical column. mkVertically :: MonadBackend m => Constructor m mkVertically = layouting snd -- | @changeField field m@ applies @m@ to the current value of the -- field @field@, updates @field@ with the value returned by @m@, and -- returns the new value. changeField :: FieldDesc s im v -> (v -> ObjectM s im v) -> ObjectM s im v changeField (ReadWriteField _ getter setter) m = do v' <- m =<< getter setter v' return v' changeField (ReadOnlyField _ _) _ = fail "Field is read-only" -- | Like 'changeField', but without a return value. changeField_ :: FieldDesc s im v -> (v -> ObjectM s im v) -> ObjectM s im () changeField_ f m = changeField f m >> return () -- | @changingFields fields m@ evaluates @m@, then emits field change -- events for those fields whose names are in @fields@ that changed -- while evaluating @m@. changingField :: (MonadBackend im, Mold v) => FieldDesc s im v -> ObjectM s im a -> ObjectM s im a changingField f m = do v <- unmold <$> getField f a <- m v' <- unmold <$> getField f changed (fieldName f) v v' return a -- | The result of using 'match' to apply a user-provided pattern to a -- string. data Match = ExactMatch | PrefixMatch | InfixMatch deriving (Eq, Ord, Show) -- | @match pat s@ applies the pattern @pat@ to @s@ and returns a -- 'Match' describing the kind of match if any, or 'Nothing' -- otherwise. The pattern is interpreted as tokens delimited by -- whitespace, and each token must be present somewhere in @s@. match :: T.Text -> T.Text -> Maybe Match match pat s | pat == s = Just ExactMatch | otherwise = case T.words pat of [] -> Just PrefixMatch pat'@(x:_) | all look pat' -> if x `T.isPrefixOf` s then Just PrefixMatch else Just InfixMatch | otherwise -> Nothing where look tok = tok `T.isInfixOf` s -- | @filterMatches f pat l@ returns only those elements of @l@ that -- match @pat@, using @f@ to convert each element to a 'T.Text'. The -- result will be ordered equivalently to @l@ filterMatches :: (a -> T.Text) -> T.Text -> [a] -> [a] filterMatches f pat = filter (isJust . match pat . f) -- | @sortMatches f pat l@ returns only those elements of @l@ that -- match @pat@, using @f@ to convert each element to a 'T.Text'. The -- result will be reordered such that exact matches come first, then -- prefixes, then infixes, although original order will be maintained -- within these three groups. sortMatches :: (a -> T.Text) -> T.Text -> [a] -> [a] sortMatches f t ts = map snd $ exacts++prefixes++infixes where attach y = do m <- match t $ f y return (m, y) matches = mapMaybe attach ts (exacts, nonexacts) = partition ((==ExactMatch) . fst) matches (prefixes, infixes) = partition ((==PrefixMatch) . fst) nonexacts