threepenny-editors 0.2.0.10 → 0.2.0.11
raw patch · 6 files changed
+173/−25 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +2/−0
- README.md +139/−2
- examples/Person.hs +13/−17
- src/Graphics/UI/Threepenny/Editors/Base.hs +3/−2
- src/Graphics/UI/Threepenny/Editors/Profunctor.hs +15/−3
- threepenny-editors.cabal +1/−1
CHANGELOG.md view
@@ -1,3 +1,5 @@+# 0.2.0.11 (2017-06-10)+ * Documentation only release. # 0.2.0.10 (2017-05-23) * Nested grids. All layouts are now grid based. # 0.2.0.9 (2017-05-23)
README.md view
@@ -2,5 +2,142 @@ [](https://hackage.haskell.org/package/threepenny-editors) [](http://stackage.org/nightly/package/threepenny-editors) -# threepenny-editors-A library for writing composable algebraic widgets with threepenny-gui. +# threepenny-editors ++## Introduction+A library allowing to easily create threepenny-gui widgets for editing algebraic datatypes. +The library provides a set of editors for primitive and base types, a set of editor+constructors for building editors for type constructors, and a set of combinators for+composing editors - the `EditorFactory` type has an `Applicative`-like structure, with two+combinators for horizontal and vertical composition, as well as+a `Profunctor` instance. Don't worry if you are not familiar with these concepts as they are+not required to perform simple tasks with this library.+```+newtype EditorFactory a b+instance Profunctor EditorFactory++(|*|) :: EditorFactory s (b->a) -> EditorFactory s b -> EditorFactory s a+(-*-) :: EditorFactory s (b->a) -> EditorFactory s b -> EditorFactory s a+```++The library also provides an `Editable` type class to associate a default `EditorFactoy` with+a type:+```+class Editable a where+ editor :: EditorFactory a a+```++## Example++Let's start with something simple, obtaining an `EditorFactory` for a newtype:+```+newtype Brexiteer = Brexiteer {unBrexiteer::Bool} deriving (Bounded, Enum, Eq, Read, Show, Ord, Generic)+```++Since we already have an `Editable` instance for `Bool` that displays a checkbox, +we can obtain an `Editable` instance for `Brexiteer` for free:+```+deriving instance Editable Brexiteer+```++We can also wrap the existing `Bool` editor manually if we want to using `dimap`:+```+editorBrexiteer = dimap unBrexiteer Brexiteer (editor :: Editor Bool Bool)+```+The type annotation above is only for illustrative purposes.++Perhaps we are not happy with the default checkbox editor and want to have a different UI?+The code below shows how to use a textbox instead:+```+editorBrexiteerText :: EditorFactory Brexiteer Brexiteer+editorBrexiteerText = editorReadShow+```+Or a combo box:+```+editorBrexiteerChoice :: EditorFactoy Brexiteer Brexiteer+editorBrexiteerChoice = editorEnumBounded+```+Let's move on to a union type now:+```+data Education+ = Basic+ | Intermediate+ | Other_ String+ deriving (Eq, Read, Show)+```+We could define an editor for `Education` with `editorReadShow`, but maybe we want a more user+friendly UI that displays a choice of education type, and only in the `Other` case a free form+text input. The `editorSum` combinator takes a list of choices and an editor for each choice:+```+editorEducation :: EditorFactory Education Education+editorEducation = do+ let selector x = case x of+ Other _ -> "Other"+ _ -> show x+ editorSum+ [ ("Basic", const Basic <$> editorUnit)+ , ("Intermediate", const Intermediate <$> editorUnit)+ , ("Other", dimap (fromMaybe "" . getOther) Other editor)+ ]+ selector++getOther :: Education -> Maybe String+getOther (Other s) = Just s+getOther _ = Nothing+```++Or more simply, we could just use `editorGeneric` to achieve the same effect, provided that+`Education` has got SOP.Generic and SOP.HasDatatypeInfo instances+```+import GHC.Generics+import qualified Generics.SOP as SOP++deriving instance Generic Education+instance SOP.HasDatatypeInfo Education+instance SOP.Generic Education++-- Derive an Editable instance that uses editorGeneric+instance Editable Education++-- Explicitly call editorGeneric+editorEducation :: EditorFactory Education Education+editorEducation = editorGeneric+```+Moving on to a record type, let's look at how to compose multiple editors together:+```+data Person = Person+ { education :: Education+ , firstName, lastName :: String+ , age :: Maybe Int+ , brexiteer :: Brexiteer+ , status :: LegalStatus+ }+ deriving (Generic, Show)+```+The `field` combinator encapsulates the common pattern of pairing a label and a base editor+to build the editor for a record field:+```+field :: String -> (out -> inn) -> EditorFactory inn a -> EditorFactory out a+field name f e = string name *| lmap f e+```+Where `*|` prepends a UI Element to an Editor horizontally: +```+(*|) :: UI Element -> EditorFactory s a -> EditorFactory s a+```+Armed with `field` and applicative composition (vertical '-*-' and horizontal '|*|'),+we define the editor for `Person` almost mechanically:+```+editorPerson :: EditorFactory Person Person+editorPerson =+ (\fn ln a e ls b -> Person e fn ln a b ls)+ <$> field "First:" firstName editor+ -*- field "Last:" lastName editor+ -*- field "Age:" age editor+ -*- field "Education:" education editorEducation+ -*- field "Status" status (editorJust $ editorSelection (pure [minBound..]) (pure (string.show)))+ -*- field "Brexiter" brexiteer editor+```+The only bit of ingenuity in the code above is the deliberate reordering of the fields.++It is also possible to generically derive the editor for person in the same way as before, in which+case the labels are taken from the field names, and the order from the declaration order.
examples/Person.hs view
@@ -28,30 +28,26 @@ instance Default LegalStatus where def = Single data Education- = Basic_- | Intermediate_- | Other_ String+ = Basic+ | Intermediate+ | Other String deriving (Eq, Ord, Read, Show, Generic) -instance Default Education where def = Basic_+instance Default Education where def = Basic getOther :: Education -> Maybe String-getOther (Other_ s) = Just s-getOther _ = Nothing--data EducationTag = Basic | Intermediate | Other deriving (Eq,Ord,Show, Generic)+getOther (Other s) = Just s+getOther _ = Nothing editorEducation :: EditorFactory Education Education editorEducation = do- let selector x =- case x of- Basic_ -> Basic- Intermediate_ -> Intermediate- Other_ _ -> Other+ let selector x = case x of+ Other _ -> "Other"+ _ -> show x editorSum- [ (Basic, const Basic_ <$> editorUnit)- , (Intermediate, const Intermediate_ <$> editorUnit)- , (Other, dimap (fromMaybe "" . getOther) Other_ editor)+ [ ("Basic", const Basic <$> editorUnit)+ , ("Intermediate", const Intermediate <$> editorUnit)+ , ("Other", dimap (fromMaybe "" . getOther) Other editor) ] selector @@ -78,7 +74,7 @@ instance Editable Person instance SOP.HasDatatypeInfo Person instance SOP.Generic Person-instance Default Person where def = Person Basic_ "First" "Last" (Just 18) def def+instance Default Person where def = Person Basic "First" "Last" (Just 18) def def editorPerson :: EditorFactory Person Person editorPerson =
src/Graphics/UI/Threepenny/Editors/Base.hs view
@@ -161,12 +161,13 @@ let t = tidings b (readIt <$> editedDef e) return $ EditorDef t (editorDefLayout e) +-- An editor that presents a choice of values. editorEnumBounded :: (Bounded a, Enum a, Ord a, Show a) => Behavior(a -> UI Element) -> Behavior (Maybe a) -> Compose UI EditorDef (Maybe a) editorEnumBounded = editorSelection (pure $ enumFrom minBound) --- | An editor that presents a dynamic list of options.+-- | An editor that presents a dynamic choice of values. editorSelection :: Ord a => Behavior [a] -> Behavior(a -> UI Element) -> Behavior (Maybe a) -> Compose UI EditorDef (Maybe a)@@ -174,7 +175,7 @@ l <- listBox options b display return $ EditorDef (tidings b (rumors $ userSelection l)) (single $ getElement l) -+-- | Ignores 'Nothing' values and only updates for 'Just' values editorJust :: (Behavior (Maybe b) -> Compose UI EditorDef (Maybe b)) -> Behavior b -> Compose UI EditorDef b
src/Graphics/UI/Threepenny/Editors/Profunctor.hs view
@@ -50,11 +50,13 @@ import Graphics.UI.Threepenny.Core import qualified Graphics.UI.Threepenny.Editors.Base as Base --- | A newtype wrapper that provides a 'Profunctor' instance.+-- | A function from 'Behavior' @a@ to 'Editor' @a@ newtype EditorFactory a b = EditorFactory { run :: Behavior a -> Compose UI Base.EditorDef b } +-- | Create an editor to display the argument.+-- User edits are fed back via the 'edited' 'Event'. createEditor :: EditorFactory b a -> Behavior b -> UI (Base.Editor a) createEditor e b = getCompose (run e b) >>= Base.runEditorDef @@ -64,6 +66,11 @@ instance Profunctor EditorFactory where dimap g h (EditorFactory f) = EditorFactory $ \b -> h <$> f (g <$> b) ++-- | The class of 'Editable' datatypes.+-- .+-- Define your own instance by using the 'Applicative' composition operators or+-- derive it via 'Generics.SOP'. class Editable a where editor :: EditorFactory a a default editor :: (Generic a, HasDatatypeInfo a, (All (All Editable `And` All Default) (Code a))) => EditorFactory a a@@ -72,6 +79,7 @@ infixl 4 |*|, -*- infixl 5 |*, *|, -*, *- +-- | Horizontal applicative composition. (|*|) :: EditorFactory s (b->a) -> EditorFactory s b -> EditorFactory s a a |*| b = EditorFactory $ \s -> run a s Base.|*| run b s @@ -81,6 +89,7 @@ (*|) :: UI Element -> EditorFactory s a -> EditorFactory s a e *| a = EditorFactory $ \s -> e Base.*| run a s +-- | Vertical applicative composition. (-*-) :: EditorFactory s (b->a) -> EditorFactory s b -> EditorFactory s a a -*- b = EditorFactory $ \s -> run a s Base.-*- run b s @@ -98,9 +107,11 @@ editorUnit :: EditorFactory a () editorUnit = EditorFactory $ \_ -> Base.editor (pure ()) +-- | An editor that presents a free form input. editorReadShow :: (Read a, Show a) => EditorFactory (Maybe a) (Maybe a) editorReadShow = EditorFactory Base.editorReadShow +-- | An editor that presents a choice of values. editorEnumBounded :: (Show a, Ord a, Enum a, Bounded a) => Behavior (a -> UI Element) -> EditorFactory (Maybe a) (Maybe a )@@ -110,7 +121,7 @@ editorJust :: EditorFactory (Maybe a) (Maybe a) -> EditorFactory a a editorJust e = EditorFactory $ Base.editorJust (run e) --- | An editor that presents a dynamic list of options+-- | An editor that presents a dynamic choice of values. editorSelection :: Ord a => Behavior [a] -> Behavior(a -> UI Element) -> EditorFactory (Maybe a) (Maybe a) editorSelection opts displ = EditorFactory $ Base.editorSelection opts displ @@ -142,7 +153,7 @@ {-------------------------------------------- Generic derivations ---------------------------------------------}-+-- | A generic editor for record types. editorGenericSimple :: forall a xs. (Generic a, HasDatatypeInfo a, All Editable xs, Code a ~ '[xs])@@ -164,6 +175,7 @@ constructorEditorFor (Constructor _) = dimap (unZ . unSOP) (SOP . Z) editor constructorEditorFor Infix{} = dimap (unZ . unSOP) (SOP . Z) editor +-- | A generic editor for SOP types. editorGeneric :: forall a . (Generic a, HasDatatypeInfo a, (All (All Editable `And` All Default) (Code a)))
threepenny-editors.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: threepenny-editors-version: 0.2.0.10+version: 0.2.0.11 synopsis: Composable algebraic editors description: This package provides a type class 'Editable' and combinators to easily put together form-like editors for algebraic datatypes.