diff --git a/examples/Main.hs b/examples/Main.hs index 0a8c6c1..712257b 100644 --- a/examples/Main.hs +++ b/examples/Main.hs @@ -18,6 +18,7 @@ import qualified ManyBoxes import qualified MenuBar import qualified Notebook import qualified Paned +import qualified Windows main :: IO () main = @@ -36,6 +37,7 @@ main = , ("CSS" , CSS.main) , ("Paned" , Paned.main) , ("Dialog" , Dialog.main) + , ("Windows" , Windows.main) ] in getArgs >>= \case [example] -> case lookup example examples of diff --git a/examples/Windows.hs b/examples/Windows.hs new file mode 100644 index 0000000..63c3bad --- /dev/null +++ b/examples/Windows.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} + +module Windows where + +import Control.Concurrent (threadDelay) +import Control.Monad (void) +import Data.Text (pack) +import Data.Vector (Vector) +import qualified Data.Vector as Vector +import Pipes.Prelude (repeatM) + +import GI.Gtk (Box (..), Button (..), + Label (..), Orientation (..), + Window (..)) +import GI.Gtk.Declarative +import GI.Gtk.Declarative.App.Simple + +type State = Vector (Maybe Int) + +data Event + = IncrAll + | AddWindow + | CloseWindow Int + | RemoveWindow + | Closed + +view' :: State -> AppView Window Event +view' ns = + bin + Window + [ #title := "Windows" + , on #deleteEvent (const (True, Closed)) + , #widthRequest := 200 + , #heightRequest := 300 + ] + $ container Box [#orientation := OrientationVertical, #spacing := 4, #margin := 4] + $ [addButton, removeButton ns] + <> Vector.imap windowLabel ns + +addButton :: BoxChild Event +addButton = BoxChild defaultBoxChildProperties + $ widget Button [#label := "Add Window", on #clicked AddWindow] + +removeButton :: State -> BoxChild Event +removeButton ns = BoxChild defaultBoxChildProperties $ widget + Button + [ #label := "Remove Window" + , #sensitive := (ns /= mempty) + , on #clicked RemoveWindow + ] + +windowLabel :: Int -> Maybe Int -> BoxChild Event +windowLabel i n = + BoxChild defaultBoxChildProperties { padding = 4 } + $ windowHost (window i <$> n) + $ windowChild i n + +window :: Int -> Int -> Bin Window Event +window i x = bin + Window + [ #title := pack ("Window " <> show i) + , on #deleteEvent (const (True, CloseWindow i)) + ] + (widget Label [#label := pack ("Open for " <> show x <> " seconds")]) + +windowChild :: Int -> Maybe Int -> Widget Event +windowChild i = \case + Nothing -> widget Label [#label := pack ("Window " <> show i <> " Closed")] + Just _ -> widget Label [#label := pack ("Window " <> show i <> " Open")] + +update' :: State -> Event -> Transition State Event +update' ns = \case + IncrAll -> Transition (fmap succ <$> ns) (return Nothing) + AddWindow -> Transition (ns `Vector.snoc` Just 1) (return Nothing) + CloseWindow i -> Transition + (Vector.imap (\i' n -> if i' == i then Nothing else n) ns) + (return Nothing) + RemoveWindow -> Transition (Vector.init ns) (return Nothing) + Closed -> Exit + +main :: IO () +main = void $ run App + { view = view' + , update = update' + , inputs = [incrPeriodically] + , initialState = [] + } + where incrPeriodically = repeatM $ IncrAll <$ threadDelay (1000 * 1000) diff --git a/examples/examples.cabal b/examples/examples.cabal index d144ce7..bbd6096 100644 --- a/examples/examples.cabal +++ b/examples/examples.cabal @@ -31,6 +31,7 @@ executable example , MenuBar , Notebook , Paned + , Windows build-depends: base >=4.10 && <5 , async , bytestring diff --git a/gi-gtk-declarative-app-simple/src/GI/Gtk/Declarative/App/Simple.hs b/gi-gtk-declarative-app-simple/src/GI/Gtk/Declarative/App/Simple.hs index 8cae180..1598e81 100644 --- a/gi-gtk-declarative-app-simple/src/GI/Gtk/Declarative/App/Simple.hs +++ b/gi-gtk-declarative-app-simple/src/GI/Gtk/Declarative/App/Simple.hs @@ -134,7 +134,7 @@ runLoop App {..} = do sub <- subscribe newMarkup newState (publishEvent events) return (newState, sub) Replace createNew -> runUI $ do - Gtk.widgetDestroy =<< someStateWidget oldState + destroy oldState oldMarkup cancel oldSubscription newState <- createNew Gtk.widgetShowAll =<< someStateWidget newState diff --git a/gi-gtk-declarative/gi-gtk-declarative.cabal b/gi-gtk-declarative/gi-gtk-declarative.cabal index 25190ef..8c20f01 100644 --- a/gi-gtk-declarative/gi-gtk-declarative.cabal +++ b/gi-gtk-declarative/gi-gtk-declarative.cabal @@ -52,6 +52,7 @@ library , GI.Gtk.Declarative.State , GI.Gtk.Declarative.Widget , GI.Gtk.Declarative.Widget.Conversions + , GI.Gtk.Declarative.WindowHost build-depends: base >=4.10 && <5 , containers >= 0.6 && < 0.7 , data-default-class >= 0.1 && <0.2 diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative.hs index 25f3d4e..5b248f1 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative.hs @@ -42,3 +42,4 @@ import GI.Gtk.Declarative.Widget as Export import GI.Gtk.Declarative.Widget.Conversions as Export ( ) +import GI.Gtk.Declarative.WindowHost as Export diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Bin.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Bin.hs index 509f1e2..657a93a 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Bin.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Bin.hs @@ -106,7 +106,7 @@ instance (Gtk.IsBin parent) => Patchable (Bin parent) where case patch oldChildState oldChild newChild of Modify modify -> SomeState . StateTreeBin top' <$> modify Replace createNew -> do - Gtk.widgetDestroy =<< someStateWidget oldChildState + destroy oldChildState oldChild newChildState <- createNew childWidget <- someStateWidget newChildState Gtk.widgetShow childWidget @@ -118,6 +118,14 @@ instance (Gtk.IsBin parent) => Patchable (Bin parent) where else Replace (create (Bin ctor newAttributes newChild)) _ -> Replace (create (Bin ctor newAttributes newChild)) + destroy (SomeState st) (Bin _ _ child) = do + case st of + StateTreeBin node childState -> do + destroy childState child + Gtk.toWidget (stateTreeWidget node) >>= Gtk.widgetDestroy + _ -> + error "Bin destroy method called with non-StateTreeBin state" + -- -- EventSource -- diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container.hs index 5ebcc0e..934661f 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container.hs @@ -127,6 +127,14 @@ instance (unChildren newChildren) else Replace (create new) _ -> Replace (create new) + + destroy (SomeState (st :: StateTree stateType w c e cs)) (Container _ _ (children :: Children child e2)) = do + case st of + StateTreeContainer top childStates -> do + sequence_ (Vector.zipWith destroy childStates (unChildren children)) + Gtk.toWidget (stateTreeWidget top) >>= Gtk.widgetDestroy + _ -> + error "Container destroy method called with non-StateTreeContainer state" -- -- EventSource diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Box.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Box.hs index e9383af..e48abe7 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Box.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Box.hs @@ -57,6 +57,7 @@ instance Patchable BoxChild where create = create . child patch s b1 b2 | properties b1 == properties b2 = patch s (child b1) (child b2) | otherwise = Replace (create b2) + destroy s b = destroy s (child b) instance EventSource BoxChild where subscribe BoxChild {..} = subscribe child diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Grid.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Grid.hs index 2ef11ed..6470b90 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Grid.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Grid.hs @@ -54,6 +54,7 @@ instance Patchable GridChild where create = create . child patch s b1 b2 | properties b1 == properties b2 = patch s (child b1) (child b2) | otherwise = Replace (create b2) + destroy s b = destroy s (child b) instance EventSource GridChild where subscribe GridChild {..} = subscribe child @@ -68,4 +69,3 @@ instance IsContainer Gtk.Grid GridChild where replaceChild grid gridChild' _i old new = do Gtk.widgetDestroy old appendChild grid gridChild' new - diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/MenuItem.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/MenuItem.hs index 9b9bf85..a98b8a0 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/MenuItem.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/MenuItem.hs @@ -66,7 +66,7 @@ menuItem -> MenuItem event menuItem item attrs = MenuItem . Bin item attrs --- | Construct a sub menu for a 'Gtk.Menu', wit a text label and the +-- | Construct a sub menu for a 'Gtk.Menu', with a text label and the -- child menu items. subMenu :: Text -> Vector (MenuItem event) -> MenuItem event subMenu label = SubMenu label . container Gtk.Menu mempty @@ -104,6 +104,14 @@ instance Patchable MenuItem where -- TODO: case for l1 /= l2 _ -> Replace (create (SubMenu l2 c2)) patch _ _ b2 = Replace (create b2) + destroy state (MenuItem (c :: Bin i e)) = + destroy state c + destroy (SomeState st) (SubMenu _ c) = case st of + StateTreeBin top childState -> do + destroy childState c + Gtk.toWidget (stateTreeWidget top) >>= Gtk.widgetDestroy + _ -> + error "Cannot destroy SubMenu with non-StateTreeBin state" instance EventSource MenuItem where subscribe (MenuItem item ) state cb = subscribe item state cb diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Paned.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Paned.hs index 233ff19..88de5b4 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Paned.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Paned.hs @@ -69,6 +69,7 @@ pane paneProperties paneChild = Pane { .. } instance Patchable Pane where create = create . paneChild patch s b1 b2 = patch s (paneChild b1) (paneChild b2) + destroy s b = destroy s (paneChild b) instance EventSource Pane where subscribe Pane {..} = subscribe paneChild diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Patch.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Patch.hs index c7ff683..db664cd 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Patch.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/Patch.hs @@ -76,11 +76,14 @@ patchInContainer (StateTreeContainer top children) container os' ns' = do -- widget in the corresponding place, we need to replace the GTK widget with -- one created from the declarative widget. (i, Just oldChildState, Nothing, Just new) -> do + error "this shouldn't happen!" + {- newChildState <- create new oldChildWidget <- someStateWidget oldChildState newChildWidget <- someStateWidget newChildState replaceChild container new i oldChildWidget newChildWidget return (Vector.singleton newChildState) + -} -- When there is a new declarative widget, or one that lacks a corresponding -- GTK widget, create and add it. @@ -92,8 +95,8 @@ patchInContainer (StateTreeContainer top children) container os' ns' = do -- When a declarative widget has been removed, remove the GTK widget from -- the container. - (_i, Just childState, Just _, Nothing) -> do - Gtk.widgetDestroy =<< someStateWidget childState + (_i, Just oldChildState, Just old, Nothing) -> do + destroy oldChildState old return Vector.empty -- When there are more old declarative widgets than GTK widgets, we can diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/CustomWidget.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/CustomWidget.hs index 3235780..e4b0f31 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/CustomWidget.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/CustomWidget.hs @@ -118,6 +118,9 @@ instance ) | otherwise -> Replace (create new) _ -> Replace (create new) + + destroy state _custom = + someStateWidget state >>= Gtk.widgetDestroy instance (Typeable internalState, Gtk.GObject widget) => diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Patch.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Patch.hs index 424133b..9c32204 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Patch.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Patch.hs @@ -34,3 +34,5 @@ class Patchable widget where -- | Given two declarative widgets of the same widget type (but not -- necessarily of the same event types,) calculate a 'Patch'. patch :: SomeState -> widget e1 -> widget e2 -> Patch + -- | Given a previously created declarative widget, destroy it. + destroy :: SomeState -> widget e -> IO () diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/SingleWidget.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/SingleWidget.hs index 8e96bf1..7f0e7d1 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/SingleWidget.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/SingleWidget.hs @@ -78,6 +78,8 @@ instance Patchable (SingleWidget widget) where ) else Replace (create (SingleWidget ctor newAttributes)) _ -> Replace (create (SingleWidget ctor newAttributes)) + destroy state _ = + someStateWidget state >>= Gtk.widgetDestroy instance EventSource (SingleWidget widget) where subscribe (SingleWidget (_ :: Gtk.ManagedPtr w1 -> w1) props) (SomeState (st :: StateTree diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/Widget.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/Widget.hs index 77ebb12..50a588d 100644 --- a/gi-gtk-declarative/src/GI/Gtk/Declarative/Widget.hs +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/Widget.hs @@ -44,6 +44,7 @@ instance Patchable Widget where patch s (Widget (w1 :: t1 e1)) (Widget (w2 :: t2 e2)) = case eqT @t1 @t2 of Just Refl -> patch s w1 w2 _ -> Replace (create w2) + destroy s (Widget w) = destroy s w instance EventSource Widget where subscribe (Widget w) = subscribe w diff --git a/gi-gtk-declarative/src/GI/Gtk/Declarative/WindowHost.hs b/gi-gtk-declarative/src/GI/Gtk/Declarative/WindowHost.hs new file mode 100644 index 0000000..f644522 --- /dev/null +++ b/gi-gtk-declarative/src/GI/Gtk/Declarative/WindowHost.hs @@ -0,0 +1,137 @@ +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE ExistentialQuantification #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedLabels #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | A wrapper around a child widget that also allows you to create a new window. +-- The window itself is not a child of this widget, nor of the parent widget, but is +-- a new top-level window: The window host just provides a place for the new window +-- to live, whilst fitting into the general tree-of-components pattern that is used +-- by gi-gtk-declarative. +module GI.Gtk.Declarative.WindowHost (windowHost) where + +import Data.Maybe (fromMaybe) +import Data.Typeable ((:~:) (..), Typeable, eqT) +import qualified GI.Gtk as Gtk +import GI.Gtk.Declarative.Bin (Bin (..)) +import GI.Gtk.Declarative.EventSource +import GI.Gtk.Declarative.Patch +import GI.Gtk.Declarative.State +import GI.Gtk.Declarative.Widget + +-- | Construct a /windowHost/ widget. +windowHost + :: Maybe (Bin Gtk.Window event) -- ^ An optional window, which will be a new top-level widget. + -> Widget event -- ^ A child widget to include in the normal widget tree. + -> Widget event -- ^ The child widget, optionally with a window linked to it. +windowHost window child = Widget $ WindowHost window child + +data WindowHost event = + WindowHost (Maybe (Bin Gtk.Window event)) (Widget event) + deriving (Functor) + +data WindowState = forall a. Typeable a => WindowState (Maybe SomeState) a + deriving (Typeable) + +instance Patchable WindowHost where + + create (WindowHost window child) = do + wrapState <$> traverse create window <*> create child + + patch state (WindowHost w1 c1) (WindowHost w2 c2) + | Just (windowState, childState) <- unwrapState state = + patch' windowState childState (w1, c1) (w2, c2) + | otherwise = + Replace . create $ WindowHost w2 c2 + + destroy state (WindowHost w c) + | Just (windowState, childState) <- unwrapState state = do + destroy childState c + case (windowState, w) of + (Just ws, Just w') -> destroy ws w' + (Nothing, Nothing) -> pure () + _ -> error "Declarative window widget and state do not match" + | otherwise = + error "Cannot destroy WindowHost with a non-WindowState state tree" + +patch' + :: Maybe SomeState + -> SomeState + -> (Maybe (Bin Gtk.Window e1), Widget e1) + -> (Maybe (Bin Gtk.Window e2), Widget e2) + -> Patch +patch' windowState childState (w1, c1) (w2, c2) = case (w1, w2, windowState) of + (Just _ , _ , Nothing ) -> error "Previous window but no previous state" + (Nothing , _ , Just _ ) -> error "Previous state but no previous window" + (Just w1', Just w2', Just windowState') -> patch'' windowState' w1' w2' + (Just w , Nothing , Just ws ) -> destroyWindow ws w + (Nothing , Just w2', Nothing ) -> modifyWindow $ create w2' + (Nothing , Nothing , Nothing ) -> keepWindow Nothing + where + patch'' :: SomeState -> Bin Gtk.Window e1 -> Bin Gtk.Window e2 -> Patch + patch'' windowState' w1' w2' = case patch windowState' w1' w2' of + Keep -> keepWindow $ Just windowState' + Modify wp -> modifyWindow wp + Replace wp -> modifyWindow $ destroy windowState' w1' *> wp + + keepWindow :: Maybe SomeState -> Patch + keepWindow windowState' = case patch childState c1 c2 of + Keep -> Keep + Modify cs -> Modify $ wrapState windowState' <$> cs + Replace cs -> Replace $ wrapState windowState' <$> cs + + modifyWindow :: IO SomeState -> Patch + modifyWindow windowPatch = case patch childState c1 c2 of + Keep -> Modify $ flip wrapState childState . Just <$> windowPatch + Modify cs -> Modify $ wrapState <$> fmap Just windowPatch <*> cs + Replace cs -> Replace $ wrapState <$> fmap Just windowPatch <*> cs + + destroyWindow :: SomeState -> Bin Gtk.Window e -> Patch + destroyWindow windowState' window = case patch childState c1 c2 of + Keep -> Modify $ wrapState Nothing childState <$ destroy windowState' window + Modify cs -> Modify $ destroy windowState' window *> (wrapState Nothing <$> cs) + Replace cs -> Replace $ destroy windowState' window *> (wrapState Nothing <$> cs) + +-- | Wrap the child state in the window state +wrapState :: Maybe SomeState -> SomeState -> SomeState +wrapState windowState (SomeState childStateTree) = case childStateTree of + StateTreeWidget node -> SomeState $ StateTreeWidget $ wrap node + StateTreeBin node child -> SomeState $ StateTreeBin (wrap node) child + StateTreeContainer node children -> + SomeState $ StateTreeContainer (wrap node) children + where + wrap :: Typeable c => StateTreeNode w e c -> StateTreeNode w e WindowState + wrap node = node + { stateTreeCustomState = WindowState windowState (stateTreeCustomState node) + } + +-- | Separate the window state and child state +unwrapState :: SomeState -> Maybe (Maybe SomeState, SomeState) +unwrapState (SomeState (st :: StateTree st w c e cs)) = + case (eqT @cs @WindowState, stateTreeCustomState $ stateTreeNode st) of + (Just Refl, WindowState ws orig) -> case st of + StateTreeWidget node -> Just + (ws, SomeState $ StateTreeWidget node { stateTreeCustomState = orig }) + StateTreeBin node child -> + Just + ( ws + , SomeState $ StateTreeBin node { stateTreeCustomState = orig } child + ) + StateTreeContainer node children -> Just + ( ws + , SomeState + $ StateTreeContainer node { stateTreeCustomState = orig } children + ) + (Nothing, _) -> Nothing + +instance EventSource WindowHost where + subscribe (WindowHost window child) state cb + | Just (windowState, childState) <- unwrapState state = do + let windowSubscribe = subscribe <$> window <*> windowState <*> pure cb + ws <- fromMaybe (pure mempty) windowSubscribe + cs <- subscribe child childState cb + pure $ ws <> cs + | otherwise = + error "Cannot subscribe to WindowHost events with a non-WindowState state tree"