packages feed

concoct (empty) → 0.1.0

raw patch · 13 files changed

+687/−0 lines, 13 filesdep +basedep +concoctdep +mtl

Dependencies added: base, concoct, mtl

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for concoct
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2026, Matt Hunzinger
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of the copyright holder nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ concoct.cabal view
@@ -0,0 +1,54 @@+cabal-version:   3.0+name:            concoct+version:         0.1.0+license:         BSD-3-Clause+license-file:    LICENSE+maintainer:      matt@hunzinger.me+author:          Matt Hunzinger+homepage:        https://github.com/matthunz/concoct+synopsis:        A declarative UI framework+description:     A declarative user-interface framework+category:        User Interfaces+build-type:      Simple+extra-doc-files: CHANGELOG.md++source-repository head+    type:     git+    location: https://github.com/matthunz/concoct.git++library+    exposed-modules:+        Concoct+        Concoct.View+        Concoct.View.Build+        Concoct.View.Internal+        Concoct.View.Rebuild+        Concoct.View.Skip+        Concoct.View.Tree+        Concoct.View.Unmount++    hs-source-dirs:   src+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.2 && <5,+        mtl >=2 && <3++executable ui+    main-is:          UI.hs+    hs-source-dirs:   examples+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.2 && <5,+        concoct++test-suite concoct-test+    type:             exitcode-stdio-1.0+    main-is:          Main.hs+    hs-source-dirs:   test+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.2 && <5,+        concoct
+ examples/UI.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE FlexibleContexts #-}++module Main where++import Concoct++counter :: (MonadView IO m) => m ()+counter = do+  count <- useState $ pure (0 :: Int)++  useEffect (readStateRef count) $ \c -> do+    putStrLn $ "Count: " ++ show c+    writeStateRef count $ c + 1++  switchView+    (even <$> readStateRef count)+    (liftView $ putStrLn "Even!")+    (liftView $ putStrLn "Odd!")++  useOnUnmount $ do+    c <- readStateRef count+    putStrLn $ "Unmount:" ++ show c++main :: IO ()+main = do+  t <- viewTree counter+  t' <- rebuildViewTree t+  _ <- unmountViewTree t'+  return ()
+ src/Concoct.hs view
@@ -0,0 +1,11 @@+-- |+-- Module      : Concoct+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct (module Concoct.View) where++import Concoct.View
+ src/Concoct/View.hs view
@@ -0,0 +1,31 @@+-- |+-- Module      : Concoct.View+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View+  ( -- * MonadView+    MonadView (..),++    -- * StateRef+    StateRef,+    readStateRef,+    writeStateRef,++    -- * ViewRef+    ViewRef,+    readViewRef,++    -- * ViewTree+    ViewTree,+    viewTree,+    rebuildViewTree,+    unmountViewTree,+  )+where++import Concoct.View.Internal+import Concoct.View.Tree
+ src/Concoct/View/Build.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}++-- |+-- Module      : Concoct.View.Build+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Build (Build (..), runBuild) where++import Concoct.View.Internal+import Control.Monad.State+import Data.IORef++newtype Build m a = Build {unBuild :: StateT ViewState m a}+  deriving (Functor, Applicative, Monad)++instance (MonadIO m) => MonadView m (Build m) where+  useState a = Build $ do+    a' <- lift a+    ref <- liftIO $ newIORef a'+    vs <- get+    let sRef = StateRef ref $ viewUpdater vs+    put vs {viewStack = pushStack sRef (viewStack vs)}+    return sRef+  useRef a = Build $ do+    a' <- lift a+    let vRef = ViewRef a'+    modify $ \vs -> vs {viewStack = pushStack vRef (viewStack vs)}+    return vRef+  useEffect deps f = Build $ do+    deps' <- lift deps+    modify $ \vs -> vs {viewStack = pushStack deps' (viewStack vs)}+    lift $ f deps'+  useOnUnmount _ = return ()+  component vb = Build $ do+    ref <- liftIO $ newIORef False+    let updater m = writeIORef ref True >> m+    vs <- get+    put vs {viewUpdater = updater, viewStack = pushStack ref (viewStack vs)}+    unBuild vb+    vs' <- get+    put vs' {viewUpdater = viewUpdater vs}+  liftView = Build . lift+  switchView cond vTrue vFalse = Build $ do+    cond' <- lift cond+    modify $ \vs -> vs {viewStack = pushStack cond' (viewStack vs)}+    if cond' then unBuild vTrue else unBuild vFalse+  listView items f = Build $ do+    items' <- lift items+    modify $ \vs -> vs {viewStack = pushStack items' (viewStack vs)}+    mapM_ (unBuild . f) items'++runBuild :: (MonadIO m) => Build m a -> ViewState -> m (a, ViewState)+runBuild vb = runStateT (unBuild vb)
+ src/Concoct/View/Internal.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE RankNTypes #-}++-- |+-- Module      : Concoct.View.Internal+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Internal+  ( -- * MonadView+    MonadView (..),++    -- * StateRef+    StateRef (..),+    readStateRef,+    writeStateRef,++    -- * ViewRef+    ViewRef (..),+    readViewRef,++    -- * Stack+    Stack (..),+    emptyStack,+    pushStack,+    popStack,+    peekStack,+    setStack,+    skipStack,+    flushStack,++    -- * ViewState+    ViewState (..),+    rebuildState,+    rebuildState',+    rebuildRef,+    rebuildRef',+    rebuildComponent,+  )+where++import Control.Monad+import Control.Monad.State+import Data.Dynamic+import Data.IORef++-- | Monadic view.+--+-- Views are interpreted in multiple passes:+--+-- * Build+-- * Rebuild+-- * Skip+-- * Unmount+--+-- And can be extended for further passes, such as layout or render.+--+-- Hooks provide access to values in a view across passes.+--+-- Views are seperated into @component@s, with each providing a scope for updates to occur.+-- A @component@ will only be rebuilt if its state is changed.+-- If a component containing children is updated, its children are rebuilt as well.+class (Monad m) => MonadView t m | m -> t where+  -- | Hook to use a mutable state reference.+  useState :: (Typeable a) => t a -> m (StateRef a)++  -- | Hook to use a constant view reference.+  useRef :: (Typeable a) => t a -> m (ViewRef a)++  -- | Hook to use an effect that runs when dependencies change.+  useEffect :: (Eq d, Typeable d) => t d -> (d -> t ()) -> m ()++  -- | Hook that runs when the current component is unmounted.+  useOnUnmount :: t () -> m ()++  -- | Component view.+  component :: (forall x. (MonadView t x) => x ()) -> m ()++  -- | Lift a monadic action into the view.+  liftView :: t () -> m ()++  -- | Conditional view.+  -- This will render the first view if the condition is @True@, otherwise the second.+  switchView ::+    t Bool ->+    (forall x. (MonadView t x) => x ()) ->+    (forall x. (MonadView t x) => x ()) ->+    m ()++  -- | List view.+  -- This will render a view for each item in the list.+  listView :: (Typeable a, Eq a) => t [a] -> (a -> (forall x. (MonadView t x) => x ())) -> m ()++-- | State reference.+-- Created with @useState@.+data StateRef a = StateRef+  { stateRef :: IORef a,+    stateRefUpdater :: IO () -> IO ()+  }++-- | Read a state reference.+readStateRef :: (MonadIO m) => StateRef a -> m a+readStateRef = liftIO . readIORef . stateRef++-- | Write to a state reference.+-- This will schedule an update to the current @component@.+writeStateRef :: (MonadIO m) => StateRef a -> a -> m ()+writeStateRef ref = liftIO . stateRefUpdater ref . writeIORef (stateRef ref)++-- | View reference.+-- Created with @useRef@.+newtype ViewRef a = ViewRef a++-- | Read a view reference.+readViewRef :: (Applicative m) => ViewRef a -> m a+readViewRef (ViewRef a) = pure a++data Stack = Stack+  { stackBefore :: [Dynamic],+    stackAfter :: [Dynamic]+  }++emptyStack :: Stack+emptyStack = Stack [] []++pushStack :: (Typeable a) => a -> Stack -> Stack+pushStack a (Stack before after) = Stack (toDyn a : before) after++popStack :: (Typeable a) => Stack -> (Maybe a, Stack)+popStack (Stack before []) = (Nothing, Stack before [])+popStack (Stack before (d : ds)) =+  case fromDynamic d of+    Just a -> (Just a, Stack (d : before) ds)+    Nothing -> popStack (Stack (d : before) ds)++peekStack :: (Typeable a) => Stack -> Maybe a+peekStack (Stack _ []) = Nothing+peekStack (Stack _ (d : _)) = fromDynamic d++setStack :: (Typeable a) => a -> Stack -> Stack+setStack a (Stack before (_ : ds)) = Stack (toDyn a : before) ds+setStack _ s = s++skipStack :: Stack -> Stack+skipStack (Stack before (d : after)) = Stack (d : before) after+skipStack s = s++flushStack :: Stack -> Stack+flushStack (Stack before after) = Stack [] (reverse before ++ after)++data ViewState = ViewState+  { viewStack :: Stack,+    viewUpdater :: IO () -> IO ()+  }++rebuildState :: (MonadIO m, Typeable a) => StateT ViewState m (StateRef a)+rebuildState = do+  vs <- get+  let (ref, s') = rebuildState' (viewStack vs)+  put vs {viewStack = s'}+  return ref++rebuildState' :: (Typeable a) => Stack -> (StateRef a, Stack)+rebuildState' s = do+  let (mref, s') = popStack s+  case mref of+    Just ref -> (ref, s')+    Nothing -> error "useState: StateRef not found in stack during rebuild"++rebuildRef :: (Monad m, Typeable a) => StateT ViewState m (ViewRef a)+rebuildRef = do+  vs <- get+  let (ref, s') = rebuildRef' (viewStack vs)+  put vs {viewStack = s'}+  return ref++rebuildRef' :: (Typeable a) => Stack -> (ViewRef a, Stack)+rebuildRef' s = do+  let (mref, s') = popStack s+  case mref of+    Just ref -> (ref, s')+    Nothing -> error "useRef: ViewRef not found in stack during rebuild"++rebuildComponent ::+  (MonadIO m, MonadView t f) =>+  (f () -> StateT ViewState m ()) ->+  (forall x. (MonadView t x) => x ()) ->+  StateT ViewState m ()+rebuildComponent g v = do+  vs <- get+  let (mref, s') = popStack (viewStack vs)+  put vs {viewStack = s'}+  case mref of+    Just ref -> do+      changed <- liftIO $ readIORef ref+      when changed $ liftIO $ writeIORef ref False+      g v+      vs' <- get+      put vs' {viewUpdater = viewUpdater vs}+    Nothing -> error "component: Change ref not found in stack during rebuild"
+ src/Concoct/View/Rebuild.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}++-- |+-- Module      : Concoct.View.Rebuild+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Rebuild (Rebuild (..), runRebuild) where++import Concoct.View.Build+import Concoct.View.Internal+import Concoct.View.Skip+import Concoct.View.Unmount+import Control.Monad+import Control.Monad.State++newtype Rebuild m a = Rebuild {unRebuild :: StateT ViewState m a}+  deriving (Functor, Applicative, Monad)++instance (MonadIO m) => MonadView m (Rebuild m) where+  useState _ = Rebuild $ rebuildState+  useRef _ = Rebuild $ rebuildRef+  useEffect deps f = Rebuild $ do+    mdep <- gets $ peekStack . viewStack+    case mdep of+      Just oldDeps -> do+        deps' <- lift deps+        when (oldDeps /= deps') $ do+          modify $ \vs -> vs {viewStack = setStack deps' (viewStack vs)}+          lift $ f deps'+      Nothing -> error "useEffect: Dependencies not found in stack during rebuild"+  useOnUnmount _ = return ()+  component v = Rebuild $ rebuildComponent unRebuild v+  liftView = Rebuild . lift+  switchView cond vTrue vFalse = Rebuild $ do+    vs <- get+    let (mcond, s') = popStack (viewStack vs)+    case mcond of+      Just oldCond -> do+        cond' <- lift cond+        if oldCond == cond'+          then do+            put vs {viewStack = pushStack cond' s'}+            if cond' then unRebuild vTrue else unRebuild vFalse+          else do+            put vs {viewStack = pushStack cond' s'}+            if oldCond+              then unSkip vTrue+              else unSkip vFalse+            if cond'+              then unBuild vTrue+              else unBuild vFalse+      Nothing -> error "switchView: Condition not found in stack during rebuild"+  listView items f = Rebuild $ do+    vs <- get+    let (mOldItems, s') = popStack (viewStack vs)+    case mOldItems of+      Just oldItems -> do+        items' <- lift items+        put vs {viewStack = pushStack items' s'}+        -- Update shared items+        let common = zip oldItems items'+            oldLen = length oldItems+            newLen = length items'+        forM_ common $ \(old, new) ->+          if old == new+            then unSkip (f old)+            else do+              unSkip (f old)+              unBuild (f new)+        -- Unmount removed items+        when (oldLen > newLen) $+          forM_ (drop newLen oldItems) $ \old -> do+            vs' <- get+            (_, stack') <- lift $ runUnmount (f old) (viewStack vs')+            put vs' {viewStack = stack'}+        -- Build added items+        when (newLen > oldLen) $+          forM_ (drop oldLen items') $ \new ->+            unBuild (f new)+      Nothing -> error "listView: Items not found in stack during rebuild"++runRebuild :: (MonadIO m) => Rebuild m a -> ViewState -> m (a, ViewState)+runRebuild vr = runStateT (unRebuild vr)
+ src/Concoct/View/Skip.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeApplications #-}++-- |+-- Module      : Concoct.View.Skip+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Skip (Skip (..), runSkip) where++import Concoct.View.Internal+import Control.Monad.State++newtype Skip m a = Skip {unSkip :: StateT ViewState m a}+  deriving (Functor, Applicative, Monad)++instance (MonadIO m) => MonadView m (Skip m) where+  useState _ = Skip rebuildState+  useRef _ = Skip rebuildRef+  useEffect _ _ = return ()+  useOnUnmount _ = return ()+  component v = Skip $ rebuildComponent unSkip v+  liftView _ = return ()+  switchView _ vTrue vFalse = Skip $ do+    vs <- get+    let (mcond, s') = popStack (viewStack vs)+    case mcond of+      Just cond' -> do+        put vs {viewStack = pushStack cond' s'}+        if cond' then unSkip vTrue else unSkip vFalse+      Nothing -> error "switchView: Condition not found in stack during skip"+  listView _ f = Skip $ do+    vs <- get+    let (mItems, s') = popStack @[_] (viewStack vs)+    case mItems of+      Just items' -> do+        put vs {viewStack = pushStack items' s'}+        mapM_ (unSkip . f) items'+      Nothing -> error "listView: Items not found in stack during skip"++runSkip :: (MonadIO m) => Skip m a -> ViewState -> m (a, ViewState)+runSkip vs = runStateT (unSkip vs)
+ src/Concoct/View/Tree.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE RankNTypes #-}++-- |+-- Module      : Concoct.View.Tree+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Tree+  ( ViewTree (..),+    viewTree,+    rebuildViewTree,+    unmountViewTree,+  )+where++import Concoct.View.Build+import Concoct.View.Internal+import Concoct.View.Rebuild+import Concoct.View.Skip+import Concoct.View.Unmount+import Control.Monad.IO.Class+import Data.IORef++-- | View tree.+data ViewTree t = ViewTree+  { viewTreeView :: forall m. (MonadView t m) => m (),+    viewTreeStack :: Stack,+    viewTreeChanged :: IORef Bool,+    viewTreePendingUpdates :: IORef (IO ())+  }++-- | Create a view tree from a view.+viewTree :: (MonadIO t) => (forall m. (MonadView t m) => m ()) -> t (ViewTree t)+viewTree v = do+  changedRef <- liftIO $ newIORef False+  pendingRef <- liftIO $ newIORef (pure ())+  let updater m = modifyIORef pendingRef (>> m) >> writeIORef changedRef True+      s = ViewState emptyStack updater+  (_, s') <- runBuild v s+  return (ViewTree v (flushStack $ viewStack s') changedRef pendingRef)++-- | Rebuild the view tree if changed.+rebuildViewTree :: (MonadIO t) => ViewTree t -> t (ViewTree t)+rebuildViewTree t = do+  pending <- liftIO $ readIORef (viewTreePendingUpdates t)+  liftIO pending+  liftIO $ writeIORef (viewTreePendingUpdates t) (pure ())+  changed <- liftIO $ readIORef (viewTreeChanged t)+  if changed+    then do+      liftIO $ writeIORef (viewTreeChanged t) False+      let updater m = modifyIORef (viewTreePendingUpdates t) (>> m) >> writeIORef (viewTreeChanged t) True+          s = ViewState (viewTreeStack t) updater+      (_, s') <- runRebuild (viewTreeView t) s+      return t {viewTreeStack = flushStack $ viewStack s'}+    else do+      let updater m = modifyIORef (viewTreePendingUpdates t) (>> m) >> writeIORef (viewTreeChanged t) True+          s = ViewState (viewTreeStack t) updater+      (_, s') <- runSkip (viewTreeView t) s+      return t {viewTreeStack = flushStack $ viewStack s'}++-- | Unmount the view tree.+unmountViewTree :: (MonadIO t) => ViewTree t -> t (ViewTree t)+unmountViewTree t = do+  (_, stack') <- runUnmount (viewTreeView t) (viewTreeStack t)+  return t {viewTreeStack = stack'}
+ src/Concoct/View/Unmount.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeApplications #-}++-- |+-- Module      : Concoct.View.Unmount+-- Copyright   : (c) Matt Hunzinger, 2026+-- License     : BSD-style (see the LICENSE file in the distribution)+--+-- Maintainer  : matt@hunzinger.me+-- Stability   : provisional+-- Portability : non-portable (GHC extensions)+module Concoct.View.Unmount (Unmount (..), runUnmount) where++import Concoct.View.Internal+import Control.Monad.State++newtype Unmount m a = Unmount {unUnmount :: StateT Stack m a}+  deriving (Functor, Applicative, Monad)++instance (MonadIO m) => MonadView m (Unmount m) where+  useState _ = Unmount $ do+    stack <- get+    let (ref, stack') = rebuildState' stack+    put stack'+    return ref+  useRef _ = Unmount $ do+    stack <- get+    let (ref, stack') = rebuildRef' stack+    put stack'+    return ref+  useEffect _ _ = Unmount $ modify skipStack+  useOnUnmount = Unmount . lift+  component v = Unmount $ do+    modify skipStack+    unUnmount v+  liftView _ = return ()+  switchView _ vTrue vFalse = Unmount $ do+    stack <- get+    let (mcond, stack') = popStack stack+    put stack'+    case mcond of+      Just cond' -> if cond' then unUnmount vTrue else unUnmount vFalse+      Nothing -> error "switchView: Condition not found in stack during unmount"+  listView _ f = Unmount $ do+    stack <- get+    let (mItems, stack') = popStack @[_] stack+    put stack'+    case mItems of+      Just items' -> mapM_ (unUnmount . f) items'+      Nothing -> error "listView: Items not found in stack during unmount"++runUnmount :: (MonadIO m) => Unmount m a -> Stack -> m (a, Stack)+runUnmount vu = runStateT (unUnmount vu)
+ test/Main.hs view
@@ -0,0 +1,4 @@+module Main (main) where++main :: IO ()+main = putStrLn "Test suite not yet implemented."