minilight 0.2.0 → 0.3.0
raw patch · 27 files changed
+1094/−387 lines, 27 filesdep +aeson-diffdep +casterdep +fsnotifydep −microlens-mtldep ~microlensdep ~vectorPVP ok
version bump matches the API change (PVP)
Dependencies added: aeson-diff, caster, fsnotify, hashtables, lens, transformers
Dependencies removed: microlens-mtl
Dependency ranges changed: microlens, vector
API changes (from Hackage documentation)
- Data.Component.AnimationLayer: [scaler] :: AnimationLayer -> Int
- Data.Component.Basic: [color] :: Config -> V4 Word8
- Data.Component.Basic: [fontDesc] :: Config -> FontDescriptor
- Data.Component.Basic: [fontSize] :: Config -> Int
- Data.Component.Basic: loadFontFrom :: Config -> MiniLight Font
- Data.Component.Layer: [position] :: Config -> V2 Int
- Data.Component.Layer: [size] :: Config -> V2 Int
- MiniLight: [components] :: LoopEnv env -> IOVector Component
- MiniLight: [env] :: LoopEnv env -> env
- MiniLight: [events] :: LoopEnv env -> IORef [Event]
- MiniLight: [keyStates] :: LoopEnv env -> HashMap Scancode Int
- MiniLight: [signalQueue] :: LoopEnv env -> IORef [Event]
- MiniLight: data LoopEnv env
- MiniLight: instance MiniLight.Light.HasLightEnv env => MiniLight.Light.HasLightEnv (Data.Text.Internal.Text, env)
- MiniLight: instance MiniLight.Light.HasLightEnv env => MiniLight.Light.HasLightEnv (MiniLight.LoopEnv env)
- MiniLight: instance MiniLight.Light.HasLoopEnv (MiniLight.LoopEnv env)
- MiniLight: instance MiniLight.Light.HasLoopEnv env => MiniLight.Light.HasLoopEnv (Data.Text.Internal.Text, env)
- MiniLight.Component: loadAppConfig :: (HasLightEnv env, MonadIO m) => FilePath -> (Text -> Value -> LightT env m Component) -> LightT env m [Component]
- MiniLight.Component: type Resolver = Text -> Value -> MiniLight Component
- MiniLight.Component.Loader: AppConfig :: [ComponentConfig] -> AppConfig
- MiniLight.Component.Loader: ComponentConfig :: Text -> Value -> ComponentConfig
- MiniLight.Component.Loader: Constant :: Value -> Expr
- MiniLight.Component.Loader: Context :: Vector (Either Int Text) -> Object -> Value -> Context
- MiniLight.Component.Loader: None :: Expr
- MiniLight.Component.Loader: Op :: Text -> Expr -> Expr -> Expr
- MiniLight.Component.Loader: Ref :: Text -> Expr
- MiniLight.Component.Loader: Var :: Text -> Expr
- MiniLight.Component.Loader: [app] :: AppConfig -> [ComponentConfig]
- MiniLight.Component.Loader: [name] :: ComponentConfig -> Text
- MiniLight.Component.Loader: [path] :: Context -> Vector (Either Int Text)
- MiniLight.Component.Loader: [properties] :: ComponentConfig -> Value
- MiniLight.Component.Loader: [target] :: Context -> Value
- MiniLight.Component.Loader: [variables] :: Context -> Object
- MiniLight.Component.Loader: convert :: Context -> Text -> Value
- MiniLight.Component.Loader: convertPath :: Text -> [Either Int Text]
- MiniLight.Component.Loader: data AppConfig
- MiniLight.Component.Loader: data ComponentConfig
- MiniLight.Component.Loader: data Context
- MiniLight.Component.Loader: data Expr
- MiniLight.Component.Loader: eval :: Context -> Expr -> Value
- MiniLight.Component.Loader: getAt :: Value -> [Either Int Text] -> Value
- MiniLight.Component.Loader: instance Data.Aeson.Types.FromJSON.FromJSON MiniLight.Component.Loader.AppConfig
- MiniLight.Component.Loader: instance Data.Aeson.Types.FromJSON.FromJSON MiniLight.Component.Loader.ComponentConfig
- MiniLight.Component.Loader: instance GHC.Classes.Eq MiniLight.Component.Loader.Expr
- MiniLight.Component.Loader: instance GHC.Generics.Generic MiniLight.Component.Loader.AppConfig
- MiniLight.Component.Loader: instance GHC.Generics.Generic MiniLight.Component.Loader.ComponentConfig
- MiniLight.Component.Loader: instance GHC.Show.Show MiniLight.Component.Loader.Expr
- MiniLight.Component.Loader: normalize :: Vector (Either Int Text) -> [Either Int Text] -> [Either Int Text]
- MiniLight.Component.Loader: parseText :: Parser a -> Text -> Result a
- MiniLight.Component.Loader: parser :: Parser Expr
- MiniLight.Component.Loader: pattern Arithmetic :: Text -> Scientific -> Scientific -> Expr
- MiniLight.Component.Loader: resolve :: Value -> Value
- MiniLight.Light: eventsL :: HasLoopEnv env => Lens' env (IORef [Event])
- MiniLight.Light: fontCacheL :: HasLightEnv env => Lens' env FontMap
- MiniLight.Light: keyStatesL :: HasLoopEnv env => Lens' env (HashMap Scancode Int)
- MiniLight.Light: rendererL :: HasLightEnv env => Lens' env Renderer
- MiniLight.Light: signalQueueL :: HasLoopEnv env => Lens' env (IORef [Event])
+ Control.Monad.Caster: LogAlert :: LogLevel
+ Control.Monad.Caster: LogCritical :: LogLevel
+ Control.Monad.Caster: LogDebug :: LogLevel
+ Control.Monad.Caster: LogEmergency :: LogLevel
+ Control.Monad.Caster: LogError :: LogLevel
+ Control.Monad.Caster: LogInfo :: LogLevel
+ Control.Monad.Caster: LogNotice :: LogLevel
+ Control.Monad.Caster: LogWarn :: LogLevel
+ Control.Monad.Caster: class MonadLogger m
+ Control.Monad.Caster: class ToBuilder a
+ Control.Monad.Caster: data LogLevel
+ Control.Monad.Caster: data LogQueue
+ Control.Monad.Caster: debug :: (MonadLogger m, MonadIO m, ToBuilder s) => s -> m ()
+ Control.Monad.Caster: err :: (MonadLogger m, MonadIO m, ToBuilder s) => s -> m ()
+ Control.Monad.Caster: getLogger :: (MonadLogger m, MonadIO m) => m LogQueue
+ Control.Monad.Caster: info :: (MonadLogger m, MonadIO m, ToBuilder s) => s -> m ()
+ Control.Monad.Caster: stdoutLogger :: LogLevel -> IO LogQueue
+ Control.Monad.Caster: toBuilder :: ToBuilder a => a -> Builder
+ Control.Monad.Caster: warn :: (MonadLogger m, MonadIO m, ToBuilder s) => s -> m ()
+ Data.Component.AnimationLayer: [interval] :: Config -> Int
+ Data.Component.Basic: wrapFigures :: Config -> [Figure] -> [Figure]
+ Data.Component.Layer: [basic] :: Config -> Config
+ Data.Component.Layer: [config] :: Layer -> Config
+ Data.Component.Selection: Config :: Config -> Vector Text -> Config -> FilePath -> Config
+ Data.Component.Selection: Selection :: Layer -> Font -> Maybe Int -> Config -> Selection
+ Data.Component.Selection: [basic] :: Config -> Config
+ Data.Component.Selection: [conf] :: Selection -> Config
+ Data.Component.Selection: [fontConfig] :: Config -> Config
+ Data.Component.Selection: [font] :: Selection -> Font
+ Data.Component.Selection: [hover] :: Selection -> Maybe Int
+ Data.Component.Selection: [image] :: Config -> FilePath
+ Data.Component.Selection: [labels] :: Config -> Vector Text
+ Data.Component.Selection: [layer] :: Selection -> Layer
+ Data.Component.Selection: data Config
+ Data.Component.Selection: data Selection
+ Data.Component.Selection: hoverL :: Lens' Selection (Maybe Int)
+ Data.Component.Selection: instance Data.Aeson.Types.FromJSON.FromJSON Data.Component.Selection.Config
+ Data.Component.Selection: instance MiniLight.Component.Types.ComponentUnit Data.Component.Selection.Selection
+ Data.Component.Selection: new :: Config -> MiniLight Selection
+ Data.Config.Font: Config :: FontDescriptor -> Int -> V4 Word8 -> Config
+ Data.Config.Font: [color] :: Config -> V4 Word8
+ Data.Config.Font: [descriptor] :: Config -> FontDescriptor
+ Data.Config.Font: [size] :: Config -> Int
+ Data.Config.Font: data Config
+ Data.Config.Font: instance Data.Aeson.Types.FromJSON.FromJSON Data.Config.Font.Config
+ Data.Config.Font: loadFontFrom :: Config -> MiniLight Font
+ Data.Registry: Registry :: reg v -> Registry v
+ Data.Registry: data Registry v
+ Data.Registry: fromList :: MonadIO m => [(Text, v)] -> m (Registry v)
+ Data.Registry: instance Data.Registry.Class.IRegistry Data.Registry.Registry
+ Data.Registry: new :: MonadIO m => m (Registry v)
+ Data.Registry.Class: (!) :: (IRegistry reg, MonadIO m) => reg v -> Text -> m v
+ Data.Registry.Class: (!?) :: (IRegistry reg, MonadIO m) => reg v -> Text -> m (Maybe v)
+ Data.Registry.Class: asVec :: IRegistry reg => reg v -> IOVector v
+ Data.Registry.Class: class IRegistry reg
+ Data.Registry.Class: delete :: (IRegistry reg, MonadIO m) => reg v -> Text -> m ()
+ Data.Registry.Class: forV_ :: (MonadIO m, IRegistry reg) => reg v -> (v -> m ()) -> m ()
+ Data.Registry.Class: has :: (IRegistry reg, MonadIO m) => reg v -> Text -> m Bool
+ Data.Registry.Class: iforV_ :: (MonadIO m, IRegistry reg) => reg v -> (Int -> v -> m ()) -> m ()
+ Data.Registry.Class: infixl 9 !
+ Data.Registry.Class: insert :: (IRegistry reg, MonadIO m) => reg v -> Int -> Text -> v -> m ()
+ Data.Registry.Class: modifyV_ :: (MonadIO m, IRegistry reg) => reg v -> (v -> m v) -> m ()
+ Data.Registry.Class: register :: (IRegistry reg, MonadIO m) => reg v -> Text -> v -> m ()
+ Data.Registry.Class: update :: (IRegistry reg, MonadIO m) => reg v -> Text -> (v -> m v) -> m ()
+ Data.Registry.Class: write :: (IRegistry reg, MonadIO m) => reg v -> Text -> v -> m ()
+ Data.Registry.HashTable: HashTableImpl :: BasicHashTable k Int -> IOVector v -> HashTableImpl k v
+ Data.Registry.HashTable: data HashTableImpl k v
+ Data.Registry.HashTable: fromListImpl :: MonadIO m => [(Text, v)] -> m (HashTableImpl Text v)
+ Data.Registry.HashTable: instance Data.Registry.Class.IRegistry (Data.Registry.HashTable.HashTableImpl Data.Text.Internal.Text)
+ Data.Vector.Mutable.PushBack: IOVector :: !IORef (IOVector a) -> !IOVector Int -> IOVector a
+ Data.Vector.Mutable.PushBack: asIOVector :: IOVector a -> IO (IOVector a)
+ Data.Vector.Mutable.PushBack: asUnsafeIOVector :: IOVector a -> IOVector a
+ Data.Vector.Mutable.PushBack: capacity :: IOVector a -> Int
+ Data.Vector.Mutable.PushBack: data IOVector a
+ Data.Vector.Mutable.PushBack: delete :: IOVector a -> Int -> IO ()
+ Data.Vector.Mutable.PushBack: fromList :: [a] -> IO (IOVector a)
+ Data.Vector.Mutable.PushBack: insert :: IOVector a -> Int -> a -> IO ()
+ Data.Vector.Mutable.PushBack: length :: IOVector a -> Int
+ Data.Vector.Mutable.PushBack: new :: Int -> IO (IOVector a)
+ Data.Vector.Mutable.PushBack: push :: IOVector a -> a -> IO ()
+ Data.Vector.Mutable.PushBack: read :: IOVector a -> Int -> IO a
+ Data.Vector.Mutable.PushBack: safeCapacity :: IOVector a -> IO Int
+ Data.Vector.Mutable.PushBack: safeLength :: IOVector a -> IO Int
+ Data.Vector.Mutable.PushBack: write :: IOVector a -> Int -> a -> IO ()
+ MiniLight: [hotConfigReplacement] :: LoopConfig -> Maybe FilePath
+ MiniLight: [light] :: LoopState -> LightEnv
+ MiniLight: [loader] :: LoopState -> LoaderEnv
+ MiniLight: [loop] :: LoopState -> LoopEnv
+ MiniLight: data LoopState
+ MiniLight: instance MiniLight.Component.Loader.HasLoaderEnv MiniLight.LoopState
+ MiniLight: instance MiniLight.Component.Loader.HasLoaderEnv env' => MiniLight.Component.Loader.HasLoaderEnv (env, env')
+ MiniLight: instance MiniLight.HasLoopState MiniLight.LoopState
+ MiniLight: instance MiniLight.Light.HasLightEnv MiniLight.LoopState
+ MiniLight: instance MiniLight.Light.HasLightEnv env' => MiniLight.Light.HasLightEnv (env, env')
+ MiniLight: instance MiniLight.Light.HasLoopEnv MiniLight.LoopState
+ MiniLight: instance MiniLight.Light.HasLoopEnv env' => MiniLight.Light.HasLoopEnv (env, env')
+ MiniLight: runMiniloop :: LoopConfig -> s -> (s -> MiniLoop s) -> MiniLight ()
+ MiniLight.Component.Internal.Resolver: Constant :: Value -> Expr
+ MiniLight.Component.Internal.Resolver: Context :: Vector (Either Int Text) -> Object -> Value -> Context
+ MiniLight.Component.Internal.Resolver: None :: Expr
+ MiniLight.Component.Internal.Resolver: Op :: Text -> Expr -> Expr -> Expr
+ MiniLight.Component.Internal.Resolver: Ref :: Text -> Expr
+ MiniLight.Component.Internal.Resolver: Var :: Text -> Expr
+ MiniLight.Component.Internal.Resolver: [path] :: Context -> Vector (Either Int Text)
+ MiniLight.Component.Internal.Resolver: [target] :: Context -> Value
+ MiniLight.Component.Internal.Resolver: [variables] :: Context -> Object
+ MiniLight.Component.Internal.Resolver: convert :: Context -> Text -> Value
+ MiniLight.Component.Internal.Resolver: convertPath :: Text -> [Either Int Text]
+ MiniLight.Component.Internal.Resolver: data Context
+ MiniLight.Component.Internal.Resolver: data Expr
+ MiniLight.Component.Internal.Resolver: eval :: Context -> Expr -> Value
+ MiniLight.Component.Internal.Resolver: getAt :: Value -> [Either Int Text] -> Value
+ MiniLight.Component.Internal.Resolver: instance GHC.Classes.Eq MiniLight.Component.Internal.Resolver.Expr
+ MiniLight.Component.Internal.Resolver: instance GHC.Show.Show MiniLight.Component.Internal.Resolver.Expr
+ MiniLight.Component.Internal.Resolver: normalize :: Vector (Either Int Text) -> [Either Int Text] -> [Either Int Text]
+ MiniLight.Component.Internal.Resolver: parseText :: Parser a -> Text -> Result a
+ MiniLight.Component.Internal.Resolver: parser :: Parser Expr
+ MiniLight.Component.Internal.Resolver: pattern Arithmetic :: Text -> Scientific -> Scientific -> Expr
+ MiniLight.Component.Internal.Resolver: resolve :: Value -> Value
+ MiniLight.Component.Internal.Types: AppConfig :: Vector ComponentConfig -> Vector Text -> AppConfig
+ MiniLight.Component.Internal.Types: ComponentConfig :: Text -> Value -> ComponentConfig
+ MiniLight.Component.Internal.Types: [app] :: AppConfig -> Vector ComponentConfig
+ MiniLight.Component.Internal.Types: [name] :: ComponentConfig -> Text
+ MiniLight.Component.Internal.Types: [properties] :: ComponentConfig -> Value
+ MiniLight.Component.Internal.Types: [uuid] :: AppConfig -> Vector Text
+ MiniLight.Component.Internal.Types: createComponentBy :: Resolver -> Maybe Text -> ComponentConfig -> MiniLight (Either String Component)
+ MiniLight.Component.Internal.Types: data AppConfig
+ MiniLight.Component.Internal.Types: data ComponentConfig
+ MiniLight.Component.Internal.Types: instance Data.Aeson.Types.FromJSON.FromJSON MiniLight.Component.Internal.Types.AppConfig
+ MiniLight.Component.Internal.Types: instance Data.Aeson.Types.FromJSON.FromJSON MiniLight.Component.Internal.Types.ComponentConfig
+ MiniLight.Component.Internal.Types: instance Data.Aeson.Types.ToJSON.ToJSON MiniLight.Component.Internal.Types.ComponentConfig
+ MiniLight.Component.Internal.Types: instance GHC.Classes.Eq MiniLight.Component.Internal.Types.AppConfig
+ MiniLight.Component.Internal.Types: instance GHC.Classes.Eq MiniLight.Component.Internal.Types.ComponentConfig
+ MiniLight.Component.Internal.Types: instance GHC.Generics.Generic MiniLight.Component.Internal.Types.ComponentConfig
+ MiniLight.Component.Internal.Types: instance GHC.Show.Show MiniLight.Component.Internal.Types.AppConfig
+ MiniLight.Component.Internal.Types: instance GHC.Show.Show MiniLight.Component.Internal.Types.ComponentConfig
+ MiniLight.Component.Internal.Types: newUID :: MonadIO m => m Text
+ MiniLight.Component.Internal.Types: type Resolver = Text " Component Type" -> Text " UID" -> Value " Component Property" -> MiniLight (Either String Component)
+ MiniLight.Component.Loader: LoaderEnv :: Registry Component -> IORef AppConfig -> LoaderEnv
+ MiniLight.Component.Loader: [appConfig] :: LoaderEnv -> IORef AppConfig
+ MiniLight.Component.Loader: [registry] :: LoaderEnv -> Registry Component
+ MiniLight.Component.Loader: _appConfig :: HasLoaderEnv c_aZhC => Lens' c_aZhC (IORef AppConfig)
+ MiniLight.Component.Loader: _registry :: HasLoaderEnv c_aZhC => Lens' c_aZhC (Registry Component)
+ MiniLight.Component.Loader: class HasLoaderEnv c_aZhC
+ MiniLight.Component.Loader: data LoaderEnv
+ MiniLight.Component.Loader: instance MiniLight.Component.Loader.HasLoaderEnv MiniLight.Component.Loader.LoaderEnv
+ MiniLight.Component.Loader: loaderEnv :: HasLoaderEnv c_aZhC => Lens' c_aZhC LoaderEnv
+ MiniLight.Component.Loader: patchAppConfig :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m) => FilePath -> Resolver -> LightT env m ()
+ MiniLight.Component.Loader: resolveConfig :: MonadIO m => FilePath -> m (Either String AppConfig)
+ MiniLight.Event: NotifyEvent :: Event -> Event
+ MiniLight.Light: LoopEnv :: HashMap Scancode Int -> MVar [Event] -> IORef [Event] -> LoopEnv
+ MiniLight.Light: [events] :: LoopEnv -> MVar [Event]
+ MiniLight.Light: [keyStates] :: LoopEnv -> HashMap Scancode Int
+ MiniLight.Light: [logger] :: LightEnv -> LogQueue
+ MiniLight.Light: [signalQueue] :: LoopEnv -> IORef [Event]
+ MiniLight.Light: _events :: HasLoopEnv c_aKBX => Lens' c_aKBX (MVar [Event])
+ MiniLight.Light: _fontCache :: HasLightEnv c_aKvN => Lens' c_aKvN FontMap
+ MiniLight.Light: _keyStates :: HasLoopEnv c_aKBX => Lens' c_aKBX (HashMap Scancode Int)
+ MiniLight.Light: _logger :: HasLightEnv c_aKvN => Lens' c_aKvN LogQueue
+ MiniLight.Light: _renderer :: HasLightEnv c_aKvN => Lens' c_aKvN Renderer
+ MiniLight.Light: _signalQueue :: HasLoopEnv c_aKBX => Lens' c_aKBX (IORef [Event])
+ MiniLight.Light: data LoopEnv
+ MiniLight.Light: instance (GHC.Base.Monad m, MiniLight.Light.HasLightEnv env) => Control.Monad.Caster.MonadLogger (MiniLight.Light.LightT env m)
+ MiniLight.Light: instance MiniLight.Light.HasLoopEnv MiniLight.Light.LoopEnv
+ MiniLight.Light: lightEnv :: HasLightEnv c_aKvN => Lens' c_aKvN LightEnv
+ MiniLight.Light: loopEnv :: HasLoopEnv c_aKBX => Lens' c_aKBX LoopEnv
- Data.Component.AnimationLayer: AnimationLayer :: Layer -> Int -> V2 Int -> Int -> Config -> AnimationLayer
+ Data.Component.AnimationLayer: AnimationLayer :: Layer -> Int -> V2 Int -> Config -> AnimationLayer
- Data.Component.AnimationLayer: Config :: Config -> V2 Int -> Config
+ Data.Component.AnimationLayer: Config :: Config -> V2 Int -> Int -> Config
- Data.Component.Basic: Config :: V2 Int -> V2 Int -> V4 Word8 -> FontDescriptor -> Int -> Config
+ Data.Component.Basic: Config :: V2 Int -> V2 Int -> Config
- Data.Component.Layer: Config :: FilePath -> V2 Int -> V2 Int -> Config
+ Data.Component.Layer: Config :: Config -> FilePath -> Config
- Data.Component.Layer: Layer :: Figure -> Layer
+ Data.Component.Layer: Layer :: Figure -> Config -> Layer
- MiniLight: LoopConfig :: Maybe [Scancode] -> Maybe FilePath -> (Text -> Value -> MiniLight Component) -> [Component] -> LoopConfig
+ MiniLight: LoopConfig :: Maybe [Scancode] -> Maybe FilePath -> Maybe FilePath -> Resolver -> [Component] -> LoopConfig
- MiniLight: LoopState :: env -> HashMap Scancode Int -> IORef [Event] -> IORef [Event] -> IOVector Component -> LoopEnv env
+ MiniLight: LoopState :: LightEnv -> LoopEnv -> LoaderEnv -> LoopState
- MiniLight: [componentResolver] :: LoopConfig -> Text -> Value -> MiniLight Component
+ MiniLight: [componentResolver] :: LoopConfig -> Resolver
- MiniLight: runMainloop :: (HasLightEnv env, HasLightEnv loop, HasLoopEnv loop, MonadIO m, MonadMask m) => (LoopEnv env -> loop) -> LoopConfig -> s -> (s -> LightT loop m s) -> LightT env m ()
+ MiniLight: runMainloop :: (HasLightEnv env, HasLightEnv env', HasLoopEnv env', HasLoaderEnv env', MonadIO m, MonadMask m) => (env -> LoopEnv -> LoaderEnv -> env') -> LoopConfig -> s -> (s -> LightT env' m s) -> LightT env m ()
- MiniLight: type MiniLoop = LightT (LoopEnv LightEnv) IO
+ MiniLight: type MiniLoop = LightT LoopState IO
- MiniLight.Component.Loader: loadAppConfig :: (HasLightEnv env, MonadIO m) => FilePath -> (Text -> Value -> LightT env m Component) -> LightT env m [Component]
+ MiniLight.Component.Loader: loadAppConfig :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m) => FilePath -> Resolver -> LightT env m ()
- MiniLight.Component.Types: newComponent :: (ComponentUnit c, HasLightEnv env, MonadIO m, MonadMask m) => c -> LightT env m Component
+ MiniLight.Component.Types: newComponent :: (ComponentUnit c, HasLightEnv env, MonadIO m, MonadMask m) => Text -> c -> LightT env m Component
- MiniLight.Light: LightEnv :: Renderer -> FontMap -> LightEnv
+ MiniLight.Light: LightEnv :: Renderer -> FontMap -> LogQueue -> LightEnv
- MiniLight.Light: class HasLightEnv env
+ MiniLight.Light: class HasLightEnv c_aKvN
- MiniLight.Light: class HasLoopEnv env
+ MiniLight.Light: class HasLoopEnv c_aKBX
Files
- CHANGELOG.md +6/−0
- README.md +67/−0
- examples/boids.hs +1/−2
- examples/button-counter.hs +3/−3
- minilight.cabal +20/−4
- src/Control/Monad/Caster.hs +37/−0
- src/Data/Component/AnimationLayer.hs +7/−7
- src/Data/Component/Basic.hs +6/−22
- src/Data/Component/Layer.hs +17/−19
- src/Data/Component/MessageEngine.hs +1/−1
- src/Data/Component/MessageLayer.hs +8/−7
- src/Data/Component/Resolver.hs +25/−19
- src/Data/Component/Selection.hs +71/−0
- src/Data/Config/Font.hs +32/−0
- src/Data/Registry.hs +38/−0
- src/Data/Registry/Class.hs +61/−0
- src/Data/Registry/HashTable.hs +49/−0
- src/Data/Vector/Mutable/PushBack.hs +93/−0
- src/MiniLight.hs +138/−117
- src/MiniLight/Component.hs +1/−9
- src/MiniLight/Component/Internal/Resolver.hs +129/−0
- src/MiniLight/Component/Internal/Types.hs +50/−0
- src/MiniLight/Component/Loader.hs +189/−130
- src/MiniLight/Component/Types.hs +5/−8
- src/MiniLight/Event.hs +2/−0
- src/MiniLight/Figure.hs +7/−8
- src/MiniLight/Light.hs +31/−31
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for minilight +## 0.3 -- 2019-05-05++* New features:+ * Hot Config Replacement+ * Stdout Logger+ ## 0.2 -- 2019-04-23 * New features:
README.md view
@@ -4,6 +4,8 @@ A simple but powerful graphics library. +*NB: This library is fairly unstable and highly experimental.*+ ## Build You first need to install [sdl2](https://www.libsdl.org/index.php) packages.@@ -13,6 +15,71 @@ ```sh ~$ sudo apt install -y libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-gfx-dev ```++## Tutorial++Create a new project. Assume that you have the `resource` directory under the project root, which contains the resource stuff.++Here is an example, specifying `resources/app.yml` for the configuration file and `resources` for the watching directory (dynamic hot reloading).++```hs+import Control.Monad+import Data.Component.Resolver (resolver)+import MiniLight++main :: IO ()+main = runLightT $ do+ runMiniloop+ ( defConfig { appConfigFile = Just "resources/app.yml"+ , hotConfigReplacement = Just "resources"+ , componentResolver = resolver+ }+ )+ ()+ return+```++In `resources/app.yml`, you can write your application configuration.++```yaml+_vars:+ window:+ width: 800+ height: 600+app:+ - name: layer+ properties:+ image: resources/background.png+ position:+ x: 0+ y: 0+ - name: message-layer+ properties:+ window:+ image: resources/window-base.png+ position:+ x: 0+ y: ${${var:window.height} - ${ref:..size.height}}+ size:+ width: ${var:window.width}+ height: 150+ next:+ image: resources/text-pause.png+ division:+ x: 1+ y: 6+ interval: 40+ engine:+ messages: This is a message.+ color: [255,255,255,255]+ font:+ family: IPAGothic+ size: 22+```++For the configuration syntax, see [MiniLight.Component](https://hackage.haskell.org/package/minilight-0.2.0/docs/MiniLight-Component.html).++For the pre-defined components, see the modules under `Data.Component`. ## Examples
examples/boids.hs view
@@ -113,8 +113,7 @@ runLightT $ do pic <- triangleOutline (Vect.V4 100 100 100 255) (Vect.V2 10 20) - runMainloop- id+ runMiniloop ( defConfig { appConfigFile = Nothing , additionalComponents = [] , componentResolver = resolver
examples/button-counter.hs view
@@ -40,10 +40,10 @@ main :: IO () main = do runLightT $ do- button <- newComponent =<< new+ uid <- newUID+ button <- newComponent uid =<< new - runMainloop- id+ runMiniloop ( defConfig { appConfigFile = Nothing , additionalComponents = [button] , componentResolver = resolver
minilight.cabal view
@@ -3,7 +3,7 @@ -- For further documentation, see http://haskell.org/cabal/users-guide/ name: minilight-version: 0.2.0+version: 0.3.0 synopsis: A SDL2-based graphics library, batteries-included. description: This package provides the wheel for a graphical application or a game.@@ -12,7 +12,7 @@ . - Figure: convenient SDL texture, once created, it can be translated or rotated during rendering. - Component: a reusable widget with event handlers, its figures can also be cached.- - dynamic component loading: view components can be constructed by an external yaml file.+ - Hot Config Replacement: view components can be constructed by an external yaml file with live preview. - built-in components: some common components are predefined. You can define a new component by yourself. -- bug-reports:@@ -32,56 +32,71 @@ library exposed-modules:+ Control.Monad.Caster+ Data.Vector.Mutable.PushBack+ Data.Registry+ Data.Registry.Class+ Data.Registry.HashTable MiniLight MiniLight.Figure MiniLight.Light MiniLight.Event MiniLight.Component MiniLight.Component.Types+ MiniLight.Component.Internal.Types+ MiniLight.Component.Internal.Resolver MiniLight.Component.Loader+ Data.Config.Font Data.Component.Basic Data.Component.Layer Data.Component.AnimationLayer Data.Component.MessageEngine Data.Component.MessageLayer Data.Component.Button+ Data.Component.Selection Data.Component.Resolver -- other-modules: -- other-extensions: build-depends:+ aeson-diff >= 1.1.0 && < 1.2, FontyFruity >= 0.5.3 && < 0.6, base >= 4.12.0 && < 4.13,+ caster >= 0.0.2.0 && < 0.1, containers >= 0.6.0 && < 0.7, text >= 1.2.3 && < 1.3, vector >= 0.12.0 && < 0.13, aeson >= 1.4.2 && < 1.5, scientific >= 0.3.6 && < 0.4, hashable >= 1.2.7 && < 1.3,+ hashtables >= 1.2.3 && < 1.3, template-haskell >= 2.14.0 && < 2.15, unordered-containers >= 0.2.10 && < 0.3, exceptions >= 0.10.1 && < 0.11, mtl >= 2.2.2 && < 2.3,+ lens >= 4.17 && < 4.18, linear >= 1.20.8 && < 1.21,- microlens >= 0.4.10 && < 0.5,- microlens-mtl >= 0.1.11 && < 0.2, sdl2 >= 2.4.1 && < 2.5, sdl2-gfx >= 0.2 && < 0.3, sdl2-image >= 2.0.0 && < 2.1, sdl2-ttf >= 2.1.0 && < 2.2,+ transformers >= 0.5.6 && < 0.6, trifecta >= 2 && < 2.1, uuid >= 1.3.12 && < 1.4, yaml >= 0.11.0 && < 0.12, mwc-random >= 0.14.0 && < 0.15,+ fsnotify >= 0.3.0 && < 0.4, hs-source-dirs: src default-language: Haskell2010 default-extensions: FlexibleContexts FlexibleInstances GADTs+ LambdaCase OverloadedStrings MultiParamTypeClasses RankNTypes Strict+ TemplateHaskell ghc-options: -Wall -Wno-name-shadowing test-suite tests@@ -96,6 +111,7 @@ tasty, tasty-hspec, trifecta,+ vector, yaml, hs-source-dirs: test default-language: Haskell2010
+ src/Control/Monad/Caster.hs view
@@ -0,0 +1,37 @@+{-| A simple logger class for LightT monad -}+module Control.Monad.Caster (+ MonadLogger (..),+ ToBuilder (..),+ LogLevel (..),+ LogQueue,+ stdoutLogger,+) where++import Control.Monad.IO.Class+import Control.Concurrent+import System.Log.Caster as Caster++class MonadLogger m where+ getLogger :: MonadIO m => m LogQueue++ debug :: (MonadIO m, ToBuilder s) => s -> m ()+ debug s = getLogger >>= \g -> Caster.debug g s++ info :: (MonadIO m, ToBuilder s) => s -> m ()+ info s = getLogger >>= \g -> Caster.info g s++ warn :: (MonadIO m, ToBuilder s) => s -> m ()+ warn s = getLogger >>= \g -> Caster.warn g s++ err :: (MonadIO m, ToBuilder s) => s -> m ()+ err s = getLogger >>= \g -> Caster.err g s++stdoutLogger :: LogLevel -> IO LogQueue+stdoutLogger level = do+ chan <- newLogChan+ q <- newLogQueue++ _ <- forkIO $ relayLog chan level terminalListener+ _ <- forkIO $ broadcastLog q chan++ return q
src/Data/Component/AnimationLayer.hs view
@@ -1,9 +1,9 @@ module Data.Component.AnimationLayer where +import Control.Lens import Control.Monad.State import Data.Aeson import Linear-import Lens.Micro import MiniLight import qualified Data.Component.Layer as Layer import qualified SDL@@ -13,32 +13,33 @@ layer :: Layer.Layer, counter :: Int, tileSize :: Vect.V2 Int,- scaler :: Int, config :: Config } instance ComponentUnit AnimationLayer where update = execStateT $ do modify $ \c -> c { counter = (counter c + 1) }- modify $ \c -> c { counter = if counter c >= (division (config c) ^._x * division (config c) ^. _y) * scaler c then 0 else counter c }+ modify $ \c -> c { counter = if counter c >= (division (config c) ^._x * division (config c) ^. _y) * interval (config c) then 0 else counter c } figures comp = do- let iv = V2 ((counter comp `div` scaler comp) `mod` division (config comp) ^. _x) ((counter comp `div` scaler comp) `div` division (config comp) ^. _x)+ let iv = V2 ((counter comp `div` interval (config comp)) `mod` division (config comp) ^. _x) ((counter comp `div` interval (config comp)) `div` division (config comp) ^. _x) return [ clip (SDL.Rectangle (SDL.P (tileSize comp * iv)) (tileSize comp)) $ Layer.layer $ layer comp ] data Config = Config { layerConf :: Layer.Config,- division :: Vect.V2 Int+ division :: Vect.V2 Int,+ interval :: Int } instance FromJSON Config where parseJSON = withObject "config" $ \v -> do conf <- parseJSON (Object v) division <- (\v -> Vect.V2 <$> v .: "x" <*> v .: "y") =<< v .: "division"+ interval <- v .:? "interval" .!= 30 - return $ Config conf division+ return $ Config conf division interval new :: Config -> MiniLight AnimationLayer new conf = do@@ -49,6 +50,5 @@ { layer = layer , counter = 0 , tileSize = div <$> size <*> division conf- , scaler = 25 , config = conf }
src/Data/Component/Basic.hs view
@@ -13,19 +13,14 @@ import Data.Aeson import Data.Aeson.Types import Data.Typeable-import Data.Word (Word8) import qualified SDL import qualified SDL.Vect as Vect-import qualified SDL.Font import MiniLight -- | Basic config type data Config = Config { size :: Vect.V2 Int,- position :: Vect.V2 Int,- color :: Vect.V4 Word8,- fontDesc :: FontDescriptor,- fontSize :: Int+ position :: Vect.V2 Int } instance FromJSON Config where@@ -38,22 +33,7 @@ position <- (\w -> maybe (return 0) w positionMaybe) $ withObject "position" $ \v -> Vect.V2 <$> v .: "x" <*> v .: "y" - color <- fmap (\[r,g,b,a] -> Vect.V4 r g b a) $ v .:? "color" .!= [255, 255, 255, 255]-- fontMaybe <- v .:? "font"- (fontDesc, fontSize) <- (\w -> maybe (return (FontDescriptor "" (FontStyle False False), 0)) w fontMaybe) $ withObject "font" $ \v -> do- family <- v .: "family"- size <- v .: "size"- bold <- v .:? "bold" .!= False- italic <- v .:? "italic" .!= False-- return $ (FontDescriptor family (FontStyle bold italic), size)-- return $ Config size position color fontDesc fontSize---- | Load a system font from 'Config' type.-loadFontFrom :: Config -> MiniLight SDL.Font.Font-loadFontFrom conf = loadFont (fontDesc conf) (fontSize conf)+ return $ Config size position -- | This wrapper function is useful when you write your component config parser. wrapConfig@@ -81,6 +61,10 @@ deriving Typeable instance EventType Signal++-- | This automatically applies basic configuration such as: position.+wrapFigures :: Config -> [Figure] -> [Figure]+wrapFigures conf = map (translate (position conf)) -- | This wrapper function is useful when you write your own 'onSignal' component. wrapSignal
src/Data/Component/Layer.hs view
@@ -1,51 +1,49 @@ module Data.Component.Layer where +import Control.Lens import Control.Monad import Data.Aeson-import Lens.Micro-import Lens.Micro.Mtl import Linear import MiniLight import qualified SDL import qualified SDL.Vect as Vect+import qualified Data.Component.Basic as Basic data Layer = Layer {- layer :: Figure+ layer :: Figure,+ config :: Config } instance ComponentUnit Layer where- figures comp = return [layer comp]+ figures comp = return $ Basic.wrapFigures (basic $ config comp) [layer comp] data Config = Config {- image :: FilePath,- size :: Vect.V2 Int,- position :: Vect.V2 Int+ basic :: Basic.Config,+ image :: FilePath } instance FromJSON Config where- parseJSON = withObject "config" $ \v -> do- image <- v .: "image"- size <- withObject "size" (\v -> Vect.V2 <$> v .: "width" <*> v .: "height") =<< v .: "size"-- positionMaybe <- v .:? "position"- position <- maybe (return 0) (withObject "position" (\v -> Vect.V2 <$> v .: "x" <*> v .: "y")) positionMaybe-- return $ Config image size position+ parseJSON = Basic.wrapConfig (\b l -> return $ Config b l) $ \v ->+ v .: "image" new :: Config -> MiniLight Layer new conf = do pic <- picture (image conf)- return $ Layer {layer = pic} + return $ Layer+ { layer = pic+ , config = conf { basic = (basic conf) { Basic.size = getFigureSize pic } }+ }+ newNineTile :: Config -> MiniLight Layer newNineTile conf = do pic <- picture $ image conf- let siz = fmap toEnum $ size conf+ let siz = fmap toEnum $ Basic.size $ basic conf let tex = texture pic let texSize = fmap toEnum $ getFigureSize pic tinfo <- SDL.queryTexture tex- renderer <- view rendererL+ renderer <- view _renderer target <- SDL.createTexture renderer (SDL.texturePixelFormat tinfo)@@ -79,5 +77,5 @@ SDL.rendererRenderTarget renderer SDL.$= Nothing tex <- fromTexture target- return $ Layer {layer = tex}+ return $ Layer {layer = tex, config = conf}
src/Data/Component/MessageEngine.hs view
@@ -1,10 +1,10 @@ module Data.Component.MessageEngine where +import Control.Lens import Control.Monad.State import Data.Aeson import qualified Data.Text as T import Data.Word (Word8)-import Lens.Micro.Mtl import MiniLight import qualified SDL import qualified SDL.Font
src/Data/Component/MessageLayer.hs view
@@ -1,11 +1,11 @@ module Data.Component.MessageLayer where +import Control.Lens import Control.Monad.State import Data.Aeson-import Lens.Micro-import Lens.Micro.Mtl import Linear import MiniLight+import qualified Data.Component.Basic as Basic import qualified Data.Component.Layer as CLayer import qualified Data.Component.AnimationLayer as CAnim import qualified Data.Component.MessageEngine as CME@@ -39,13 +39,14 @@ cursorLayer <- figures $ cursor comp textLayer <- figures $ messageEngine comp - let cursorSize = div <$> CAnim.tileSize (cursor comp) <*> CAnim.division (CAnim.config (cursor comp))- let windowSize = CLayer.size $ window $ config comp+ let cursorSize = CAnim.tileSize (cursor comp)+ let windowSize = Basic.size $ CLayer.basic $ window $ config comp+ let position = Basic.position $ CLayer.basic $ window $ config comp return- $ map (translate (CLayer.position (window (config comp)))) $ baseLayer- ++ map (translate (Vect.V2 20 10)) textLayer- ++ map (translate (Vect.V2 ((windowSize ^. _x - cursorSize ^. _x) `div` 2) (windowSize ^. _y - cursorSize ^. _y))) cursorLayer+ $ baseLayer+ ++ map (translate (position + Vect.V2 20 10)) textLayer+ ++ map (translate (position + Vect.V2 ((windowSize ^. _x - cursorSize ^. _x) `div` 2) (windowSize ^. _y - cursorSize ^. _y))) cursorLayer data Config = Config { engine :: CME.Config,
src/Data/Component/Resolver.hs view
@@ -5,7 +5,8 @@ foldResult, ) where -import qualified Data.Aeson as Aeson+import Control.Monad+import Data.Aeson import qualified Data.Text as T import MiniLight import qualified Data.Component.AnimationLayer as AnimationLayer@@ -13,26 +14,31 @@ import qualified Data.Component.Layer as Layer import qualified Data.Component.MessageEngine as MessageEngine import qualified Data.Component.MessageLayer as MessageLayer+import qualified Data.Component.Selection as Selection -foldResult :: (String -> b) -> (a -> b) -> Aeson.Result a -> b+foldResult :: (String -> b) -> (a -> b) -> Result a -> b foldResult f g r = case r of- Aeson.Error err -> f err- Aeson.Success a -> g a+ Error err -> f err+ Success a -> g a +resultM+ :: Result a+ -> (a -> MiniLight Component)+ -> MiniLight (Either String Component)+resultM r m = foldResult (return . Left) (fmap Right . m) r+ -- | Pre-defined resolver supports all components in this library. resolver :: Resolver-resolver name props = case name of- "animation-layer" -> newComponent- =<< AnimationLayer.new (foldResult error id $ Aeson.fromJSON props)- "button" ->- newComponent =<< Button.new (foldResult error id $ Aeson.fromJSON props)- "layer" ->- newComponent =<< Layer.new (foldResult error id $ Aeson.fromJSON props)- "message-engine" -> newComponent- =<< MessageEngine.new (foldResult error id $ Aeson.fromJSON props)- "message-layer" -> newComponent- =<< MessageLayer.new (foldResult error id $ Aeson.fromJSON props)- "tiled-layer" -> newComponent- =<< Layer.newNineTile (foldResult error id $ Aeson.fromJSON props)- _ -> error $ T.unpack $ "Component not defined: `" <> name <> "`"-+resolver name uid props = case name of+ "animation-layer" ->+ resultM (fromJSON props) $ newComponent uid <=< AnimationLayer.new+ "button" -> resultM (fromJSON props) $ newComponent uid <=< Button.new+ "layer" -> resultM (fromJSON props) $ newComponent uid <=< Layer.new+ "message-engine" ->+ resultM (fromJSON props) $ newComponent uid <=< MessageEngine.new+ "message-layer" ->+ resultM (fromJSON props) $ newComponent uid <=< MessageLayer.new+ "tiled-layer" ->+ resultM (fromJSON props) $ newComponent uid <=< Layer.newNineTile+ "selection" -> resultM (fromJSON props) $ newComponent uid <=< Selection.new+ _ -> return $ Left $ "Unsupported component: " ++ T.unpack name
+ src/Data/Component/Selection.hs view
@@ -0,0 +1,71 @@+module Data.Component.Selection where++import Control.Lens+import Control.Monad.State+import Data.Aeson hiding ((.=))+import qualified Data.Config.Font as Font+import qualified Data.Text as T+import qualified Data.Vector as V+import Linear+import MiniLight+import qualified SDL.Font+import qualified SDL.Vect as Vect+import qualified Data.Component.Basic as Basic+import qualified Data.Component.Layer as Layer++data Selection = Selection {+ layer :: Layer.Layer,+ font :: SDL.Font.Font,+ hover :: Maybe Int,+ conf :: Config+}++hoverL :: Lens' Selection (Maybe Int)+hoverL = lens hover (\env r -> env { hover = r })++instance ComponentUnit Selection where+ update = return++ figures comp = do+ let p = Vect.V2 15 10+ textTextures <- V.forM (V.indexed $ labels $ conf comp) $ \(i,label) -> liftMiniLight $ fmap (translate (p + Vect.V2 0 (i * 30))) $ text (font comp) (Font.color $ fontConfig $ conf comp) label+ base <- figures (layer comp)+ highlight <- liftMiniLight $ rectangleFilled (Vect.V4 240 240 240 40) $ _y .~ 30 $ Basic.size (basic (conf comp))++ return $ base+ ++ map (translate (Basic.position $ basic $ conf comp)) (translate (Vect.V2 0 (maybe 0 id (hover comp) * 30 + p ^. _y)) highlight+ : V.toList textTextures)++ useCache c1 c2 = hover c1 == hover c2++ onSignal = Basic.wrapSignal (basic . conf) $ \ev sel -> flip execStateT sel $ do+ uid <- view uidL++ case ev `asSignal` uid of+ Just (Basic.MouseOver pos) | (pos ^. _y) `div` 30 <= V.length (labels (conf sel)) - 1 -> do+ hoverL .= Just ((pos ^. _y) `div` 30)+ _ -> return ()++ -- OMG+ beforeClearCache _ figs = mapM_ freeFigure $ tail figs++data Config = Config {+ basic :: Basic.Config,+ labels :: V.Vector T.Text,+ fontConfig :: Font.Config,+ image :: FilePath+}++instance FromJSON Config where+ parseJSON = withObject "selection" $ \v ->+ Config+ <$> parseJSON (Object v)+ <*> v .:? "labels" .!= V.empty+ <*> parseJSON (Object v)+ <*> v .: "image"++new :: Config -> MiniLight Selection+new conf = do+ font <- Font.loadFontFrom (fontConfig conf)+ layer <- Layer.newNineTile $ Layer.Config (basic conf) (image conf)+ return $ Selection {font = font, conf = conf, hover = Nothing, layer = layer}
+ src/Data/Config/Font.hs view
@@ -0,0 +1,32 @@+module Data.Config.Font where++import Data.Aeson+import Data.Word (Word8)+import MiniLight+import qualified SDL.Font+import qualified SDL.Vect as Vect++data Config = Config {+ descriptor :: FontDescriptor,+ size :: Int,+ color :: Vect.V4 Word8+}++instance FromJSON Config where+ parseJSON = withObject "font" $ \v -> do+ fontMaybe <- v .:? "font"++ case fontMaybe of+ Nothing -> return $ Config (FontDescriptor "" (FontStyle False False)) 0 (Vect.V4 0 0 0 255)+ Just w -> flip (withObject "font") w $ \v -> do+ family <- v .: "family"+ size <- v .: "size"+ bold <- v .:? "bold" .!= False+ italic <- v .:? "italic" .!= False+ [r,g,b,a] <- v .:? "color" .!= [0, 0, 0, 255]++ return $ Config (FontDescriptor family (FontStyle bold italic)) size (Vect.V4 r g b a)++-- | Load a system font from 'Config' type.+loadFontFrom :: Config -> MiniLight SDL.Font.Font+loadFontFrom conf = loadFont (descriptor conf) (size conf)
+ src/Data/Registry.hs view
@@ -0,0 +1,38 @@+{-| This module provides a registry for the specific type with the Text key.++This module is intended to be imported @qualified@, like:+@+import qualified Data.Registry as R+@+-}+module Data.Registry (+ module Data.Registry.Class,++ Registry(..),+ fromList,+ new,+) where++import Control.Monad.IO.Class+import qualified Data.Text as T+import Data.Registry.Class+import Data.Registry.HashTable++-- | The @Registry@ type can represents any 'IRegistry' instance.+data Registry v = forall reg. IRegistry reg => Registry (reg v)++instance IRegistry Registry where+ (!?) (Registry reg) t = (!?) reg t+ asVec (Registry reg) = asVec reg+ write (Registry reg) k v = write reg k v+ register (Registry reg) k v = register reg k v+ insert (Registry reg) i k v = insert reg i k v+ delete (Registry reg) k = delete reg k++-- | /O(n)/ Create a registry from a list. The current implementation uses a hashtable, defined in the module 'Data.Registry.HashTable'.+fromList :: MonadIO m => [(T.Text, v)] -> m (Registry v)+fromList xs = fmap Registry $ fromListImpl xs++-- | Create a new empty registry.+new :: MonadIO m => m (Registry v)+new = fromList []
+ src/Data/Registry/Class.hs view
@@ -0,0 +1,61 @@+module Data.Registry.Class where++import Control.Monad+import Control.Monad.IO.Class+import Data.Maybe (isJust)+import qualified Data.Text as T+import qualified Data.Vector.Mutable as VM++-- | @IRegistry@ typeclass presents a registry interface.+-- The complexity /O(1)/ in the operations can be "amortized" complexity.+class IRegistry reg where+ -- | /O(1)/ Checking if the specified key exists+ has :: MonadIO m => reg v -> T.Text -> m Bool+ has reg k = fmap isJust $ reg !? k++ -- | /O(1)/ Indexing+ (!) :: MonadIO m => reg v -> T.Text -> m v+ reg ! k = fmap (\(Just a) -> a) $ reg !? k++ -- | /O(1)/ Safe indexing+ (!?) :: MonadIO m => reg v -> T.Text -> m (Maybe v)++ -- | /O(1)/ Update, raise an exception if the key does not exist.+ update :: MonadIO m => reg v -> T.Text -> (v -> m v) -> m ()+ update reg k f = reg ! k >>= \v -> f v >>= \v' -> write reg k v'++ -- | /O(1)/ Write, raise an exception if the key does not exist.+ write :: MonadIO m => reg v -> T.Text -> v -> m ()++ -- | /O(1)/ Adding a new value to the last position+ register :: MonadIO m => reg v -> T.Text -> v -> m ()++ -- | /O(n)/ Inserting a new value to the specified position (in the underlying vector)+ insert :: MonadIO m => reg v -> Int -> T.Text -> v -> m ()++ -- | /O(n)/ Deleting the specified value (this is a slow operation).+ delete :: MonadIO m => reg v -> T.Text -> m ()++ -- | /O(1)/ Get the underlying vector. Be careful: modifying the vector might cause a problem.+ asVec :: reg v -> VM.IOVector v++infixl 9 !+infixl 9 !?++-- | For-loop over the registry, ignoring the key order.+forV_ :: (MonadIO m, IRegistry reg) => reg v -> (v -> m ()) -> m ()+forV_ reg iter =+ let vec = asVec reg+ in forM_ [0 .. VM.length vec - 1] $ \i -> liftIO (VM.read vec i) >>= iter++-- | For-loop over the registry with the index, ignoring the key order.+iforV_ :: (MonadIO m, IRegistry reg) => reg v -> (Int -> v -> m ()) -> m ()+iforV_ reg iter =+ let vec = asVec reg+ in forM_ [0 .. VM.length vec - 1] $ \i -> liftIO (VM.read vec i) >>= iter i++-- | Modifying the item one by one, ignoring the key order.+modifyV_ :: (MonadIO m, IRegistry reg) => reg v -> (v -> m v) -> m ()+modifyV_ reg iter = iforV_ reg $ \i v -> do+ v' <- iter v+ liftIO $ VM.write (asVec reg) i v'
+ src/Data/Registry/HashTable.hs view
@@ -0,0 +1,49 @@+{-| Registry implementation using hashtable+-}+module Data.Registry.HashTable where++import Control.Monad.IO.Class+import qualified Data.HashTable.IO as H+import Data.Foldable (foldlM)+import Data.Maybe (fromJust)+import Data.Registry.Class+import qualified Data.Text as T+import qualified Data.Vector.Mutable.PushBack as VMP++data HashTableImpl k v = HashTableImpl (H.BasicHashTable k Int) (VMP.IOVector v)++instance IRegistry (HashTableImpl T.Text) where+ (!?) (HashTableImpl reg vec) k = liftIO $ do+ i <- H.lookup reg k+ maybe (return Nothing) (fmap Just . VMP.read vec) i++ write (HashTableImpl ht vec) k v = liftIO $ do+ i <- fmap fromJust $ liftIO $ H.lookup ht k+ liftIO $ VMP.write vec i v++ register (HashTableImpl ht vec) k v = liftIO $ do+ len <- VMP.safeLength vec+ VMP.push vec v+ H.insert ht k len++ delete (HashTableImpl ht vec) k = liftIO $ do+ Just i <- H.lookup ht k+ VMP.delete vec i+ H.delete ht k++ insert (HashTableImpl ht vec) i k v = liftIO $ do+ VMP.insert vec i v+ H.insert ht k i++ asVec (HashTableImpl _ vec) = VMP.asUnsafeIOVector vec++fromListImpl :: MonadIO m => [(T.Text, v)] -> m (HashTableImpl T.Text v)+fromListImpl xs =+ liftIO+ $ HashTableImpl+ <$> ( H.new >>= \h -> foldlM+ (\acc (i, k) -> H.insert acc k i >> return acc)+ h+ (zip [0 ..] $ map fst xs)+ )+ <*> (VMP.fromList $ map snd xs)
+ src/Data/Vector/Mutable/PushBack.hs view
@@ -0,0 +1,93 @@+{-| This module provides a variant of the vector, equipped with @push_back@ operation.+IOVector here are supposed to be used in single thread situation.+-}+module Data.Vector.Mutable.PushBack where++import Prelude hiding (length, read)+import Control.Monad+import Data.IORef+import qualified Data.Vector as V+import qualified Data.Vector.Mutable as VM+import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM+import System.IO.Unsafe++-- | @IOVector@ consists of (1) pointer to the underlying vector (2) length+-- While 'Data.Vector' has the underlying array itself, this type only has the pointer.+-- This means read/write should be slower than the original vector.+data IOVector a = IOVector !(IORef (VM.IOVector a)) !(VUM.IOVector Int)++-- Allocate (p + 10)-element vector, which might be more efficient than allocating just a small size of vector, like 1-element or 2-element.+new :: Int -> IO (IOVector a)+new p = new' (p + 10)+ where new' p = IOVector <$> (newIORef =<< VM.new p) <*> (VUM.replicate 1 0)++read :: IOVector a -> Int -> IO a+read (IOVector vref _) k = readIORef vref >>= \vec -> VM.read vec k++-- | Get the position of the last cell in the @IOVector@. This operation is not safe because of the 'unsafePerformIO'.+safeLength :: IOVector a -> IO Int+safeLength (IOVector _ uvec) = VUM.read uvec 0++length :: IOVector a -> Int+length pvec = unsafePerformIO $ safeLength pvec++safeCapacity :: IOVector a -> IO Int+safeCapacity (IOVector vref _) = fmap VM.length $ readIORef vref++-- | Get the capacity of the @IOVector@. This operation is not safe because of the 'unsafePerformIO'.+capacity :: IOVector a -> Int+capacity pvec = unsafePerformIO $ safeCapacity pvec++write :: IOVector a -> Int -> a -> IO ()+write (IOVector vref _) i v = do+ vec <- readIORef vref+ VM.write vec i v++-- | /O(n)/ Insert a value into any place. This is a slow operation.+insert+ :: IOVector a -- ^ The vector should have positive (non-zero) length+ -> Int+ -> a+ -> IO ()+insert pvec@(IOVector _ uvec) i v = do+ len <- safeLength pvec++ read pvec (len - 1) >>= push pvec+ forM_ (reverse [i .. len - 2]) $ \j -> read pvec j >>= write pvec (j + 1)+ write pvec i v++-- | /O(n)/ This is a slow operation. This also throws an exception if the specified index does not exist.+delete :: IOVector a -> Int -> IO ()+delete pvec@(IOVector _ uvec) i = do+ len <- safeLength pvec+ forM_ [i + 1 .. len - 1] $ \j -> read pvec j >>= write pvec (j - 1)+ VUM.modify uvec (\x -> x - 1) 0++push :: IOVector a -> a -> IO ()+push pvec@(IOVector vref uvec) v = do+ vec <- readIORef vref+ len <- safeLength pvec+ cap <- safeCapacity pvec+ when (len == cap) $ do+ vec' <- VM.grow vec cap+ writeIORef vref vec'++ write pvec len v+ VUM.modify uvec (+ 1) 0++fromList :: [a] -> IO (IOVector a)+fromList xs = do+ vec <- V.thaw $ V.fromList xs+ vec' <- VM.grow vec ((VM.length vec + 5) * 2)+ vref <- newIORef vec'+ uvec <- VU.thaw $ VU.fromList [VM.length vec]+ return $ IOVector vref uvec++asIOVector :: IOVector a -> IO (VM.IOVector a)+asIOVector pvec@(IOVector vref _) = do+ len <- safeLength pvec+ readIORef vref >>= \vec -> return (VM.slice 0 len vec)++asUnsafeIOVector :: IOVector a -> VM.IOVector a+asUnsafeIOVector pvec = unsafePerformIO $ asIOVector pvec
src/MiniLight.hs view
@@ -1,5 +1,6 @@ {-| MiniLight module exports all basic concepts and oprations except for concrete components. -}+{-# LANGUAGE FunctionalDependencies #-} module MiniLight ( module MiniLight.Light, module MiniLight.Event,@@ -7,30 +8,34 @@ module MiniLight.Component, runLightT,+ LoopState (..), LoopConfig (..), defConfig,- LoopEnv (..),- MiniLoop, runMainloop,+ MiniLoop,+ runMiniloop, ) where -import Control.Concurrent (threadDelay)+import Control.Concurrent (threadDelay, forkIO)+import Control.Concurrent.MVar+import Control.Lens+import qualified Control.Monad.Caster as Caster import Control.Monad.Catch import Control.Monad.Reader-import qualified Data.Aeson as Aeson import Data.Foldable (foldlM) import Data.Hashable (Hashable(..)) import qualified Data.HashMap.Strict as HM+import Data.Maybe import Data.IORef+import qualified Data.Registry as R+import qualified Data.Vector as V import qualified Data.Text as T-import qualified Data.Vector.Mutable as VM import Graphics.Text.TrueType-import Lens.Micro-import Lens.Micro.Mtl import MiniLight.Component import MiniLight.Event import MiniLight.Figure import MiniLight.Light+import qualified System.FSNotify as Notify import qualified SDL import qualified SDL.Font @@ -42,13 +47,24 @@ runLightT prog = withSDL $ withWindow $ \window -> do renderer <- SDL.createRenderer window (-1) SDL.defaultRenderer fc <- loadFontCache- runReaderT (runLightT' prog) $ LightEnv {renderer = renderer, fontCache = fc}+ logger <- liftIO $ Caster.stdoutLogger Caster.LogDebug+ runReaderT (runLightT' prog)+ $ LightEnv {renderer = renderer, fontCache = fc, logger = logger}+ where+ withSDL =+ bracket (SDL.initializeAll >> SDL.Font.initialize)+ (\_ -> SDL.Font.quit >> SDL.quit)+ . const + withWindow =+ bracket (SDL.createWindow "window" SDL.defaultWindow) SDL.destroyWindow+ -- | Use 'defConfig' for a default setting. data LoopConfig = LoopConfig { watchKeys :: Maybe [SDL.Scancode], -- ^ Set @Nothing@ if all keys should be watched. See also 'LoopState'. appConfigFile :: Maybe FilePath, -- ^ Specify a yaml file which describes component settings. See 'MiniLight.Component' for the yaml syntax.- componentResolver :: T.Text -> Aeson.Value -> MiniLight Component, -- ^ Your custom mappings between a component name and its type.+ hotConfigReplacement :: Maybe FilePath, -- ^ The directory path to be watched. If set, the config file modification will replace the component dynamically.+ componentResolver :: Resolver, -- ^ Your custom mappings between a component name and its type. additionalComponents :: [Component] -- ^ The components here would be added during the initialization. } @@ -57,111 +73,115 @@ defConfig = LoopConfig { watchKeys = Nothing , appConfigFile = Nothing+ , hotConfigReplacement = Nothing , componentResolver = \_ _ -> undefined , additionalComponents = [] } -fromList :: MonadIO m => [a] -> m (VM.IOVector a)-fromList xs = liftIO $ do- vec <- VM.new $ length xs- forM_ (zip [0 ..] xs) $ uncurry (VM.write vec)- return vec---- | LoopEnv value would be passed to user side in a mainloop.-data LoopEnv env = LoopState {- env :: env,- keyStates :: HM.HashMap SDL.Scancode Int,- events :: IORef [Event],- signalQueue :: IORef [Event],- components :: VM.IOVector Component+-- | The state in the mainloop.+data LoopState = LoopState {+ light :: LightEnv,+ loop :: LoopEnv,+ loader :: LoaderEnv } --- | Lens to the env inside 'LoopState'-envL :: Lens' (LoopEnv env) env-envL = lens env (\e r -> e { env = r })+makeLensesWith classyRules_ ''LoopState -instance HasLightEnv env => HasLightEnv (LoopEnv env) where- rendererL = envL . rendererL- fontCacheL = envL . fontCacheL+-- | Type synonym to the minimal type of the mainloop+type MiniLoop = LightT LoopState IO -instance HasLoopEnv (LoopEnv env) where- keyStatesL = lens keyStates (\env r -> env { keyStates = r })- eventsL = lens events (\env r -> env { events = r })- signalQueueL = lens signalQueue (\env r -> env { signalQueue = r }) -instance HasLightEnv env => HasLightEnv (T.Text, env) where- rendererL = _2 . rendererL- fontCacheL = _2 . fontCacheL+-- These instances are used in the internal computation.+instance HasLightEnv LoopState where+ lightEnv = _light . lightEnv -instance HasLoopEnv env => HasLoopEnv (T.Text, env) where- keyStatesL = _2 . keyStatesL- eventsL = _2 . eventsL- signalQueueL = _2 . signalQueueL+instance HasLoopEnv LoopState where+ loopEnv = _loop . loopEnv +instance HasLoaderEnv LoopState where+ loaderEnv = _loader . loaderEnv+++instance HasLightEnv env' => HasLightEnv (env, env') where+ lightEnv = _2 . lightEnv++instance HasLoopEnv env' => HasLoopEnv (env, env') where+ loopEnv = _2 . loopEnv++instance HasLoaderEnv env' => HasLoaderEnv (env, env') where+ loaderEnv = _2 . loaderEnv++ instance HasComponentEnv (T.Text, env) where uidL = _1 --- | Type synonym to the minimal type of the mainloop-type MiniLoop = LightT (LoopEnv LightEnv) IO +-- | Same as 'runMainloop' but fixing the type.+runMiniloop :: LoopConfig -> s -> (s -> MiniLoop s) -> MiniLight ()+runMiniloop = runMainloop LoopState+ -- | Run a mainloop. -- In a mainloop, components and events are managed. -- -- Components in a mainloop: draw ~ update ~ (user-defined function) ~ event handling runMainloop :: ( HasLightEnv env- , HasLightEnv loop- , HasLoopEnv loop+ , HasLightEnv env'+ , HasLoopEnv env'+ , HasLoaderEnv env' , MonadIO m , MonadMask m )- => (LoopEnv env -> loop) -- ^ LoopState conversion function (you can pass @id@, fixing @loop@ as @'LoopState' 'LightEnv'@)- -> LoopConfig -- ^ loop config- -> s -- ^ initial state- -> (s -> LightT loop m s) -- ^ a function called in every loop+ => (env -> LoopEnv -> LoaderEnv -> env') -- ^ Environment conversion+ -> LoopConfig -- ^ Loop config+ -> s -- ^ Initial state+ -> (s -> LightT env' m s) -- ^ A function called in every loop -> LightT env m ()-runMainloop conv conf initial loop = do- components <-- liftMiniLight $ fromList . (++ additionalComponents conf) =<< maybe- (return [])- (flip loadAppConfig (componentResolver conf))- (appConfigFile conf)- events <- liftIO $ newIORef []+runMainloop conv conf initial userloop = do+ events <- liftIO $ newMVar [] signalQueue <- liftIO $ newIORef []+ reg <- R.new+ conf <- liftIO $ newIORef $ AppConfig V.empty V.empty - env <- view id- go- ( LoopState- { keyStates = HM.empty- , events = events- , signalQueue = signalQueue- , env = env- , components = components- }- )+ run+ (LoopEnv {keyStates = HM.empty, events = events, signalQueue = signalQueue})+ (LoaderEnv {registry = reg, appConfig = conf}) initial where- go loopState s = do- renderer <- view rendererL+ run loop loader s = do+ setup loop loader+ go loop loader s++ setup loop loader = envLightT (\env -> conv env loop loader) $ do+ case (hotConfigReplacement conf, appConfigFile conf) of+ (Just dir, Just confPath) -> do+ liftIO $ forkIO $ Notify.withManager $ \mgr -> do+ _ <- Notify.watchDir mgr dir (const True) $ \ev -> do+ modifyMVar_ (loop ^. _events) $ return . (NotifyEvent ev :)++ forever $ threadDelay 1000000++ loadAppConfig confPath (componentResolver conf)+ _ -> return ()++ forM_ (additionalComponents conf) $ \component -> do+ reg <- view _registry+ R.register reg (getUID component) component++ go loop loader s = do+ renderer <- view _renderer liftIO $ SDL.rendererDrawColor renderer SDL.$= 255 liftIO $ SDL.clear renderer - forM_ [0 .. VM.length (components loopState) - 1] $ \i -> do- comp <- liftIO $ VM.read (components loopState) i- draw comp+ R.forV_ (loader ^. _registry) $ \comp -> draw comp -- state propagation- forM_ [0 .. VM.length (components loopState) - 1] $ \i -> do- comp <- liftIO $ VM.read (components loopState) i- liftIO $ VM.write (components loopState) i (propagate comp)+ R.modifyV_ (loader ^. _registry) $ return . propagate - forM_ [0 .. VM.length (components loopState) - 1] $ \i -> do- comp <- liftIO $ VM.read (components loopState) i- comp' <- envLightT (\env -> (getUID comp, conv $ loopState { env = env }))- $ update comp- liftIO $ VM.write (components loopState) i comp'+ R.modifyV_ (loader ^. _registry) $ \comp ->+ envLightT (\env -> (getUID comp, conv env loop loader)) $ update comp - s' <- envLightT (\env -> conv $ loopState { env = env }) $ loop s+ s' <- envLightT (\env -> conv env loop loader) $ userloop s liftIO $ SDL.present renderer @@ -169,37 +189,50 @@ events <- SDL.pollEvents keys <- SDL.getKeyboardState - envLightT (\env -> conv $ loopState { env = env }) $ do- evref <- view eventsL- liftIO $ writeIORef evref $ map RawEvent events-- sigref <- view signalQueueL+ envLightT (\env -> conv env loop loader) $ do+ evref <- view _events+ sigref <- view _signalQueue signals <- liftIO $ readIORef sigref- liftIO $ modifyIORef evref $ (++ signals)+ liftIO+ $ modifyMVar_ evref+ $ return+ . (map RawEvent events ++)+ . (signals ++) liftIO $ writeIORef sigref [] - envLightT (\env -> conv $ loopState { env = env }) $ do- evref <- view eventsL- events <- liftIO $ readIORef evref+ envLightT (\env -> conv env loop loader) $ do+ evref <- view _events+ events <- liftIO $ modifyMVar evref (\a -> return ([], a)) - forM_ [0 .. VM.length (components loopState) - 1] $ \i -> do- comp <- liftIO $ VM.read (components loopState) i- comp' <- foldlM- (\comp ev -> envLightT ((,) (getUID comp)) $ onSignal ev comp)- comp- events- liftIO $ VM.write (components loopState) i comp'+ R.modifyV_ (loader ^. _registry) $ \comp -> do+ foldlM (\comp ev -> envLightT ((,) (getUID comp)) $ onSignal ev comp)+ comp+ events - let- specifiedKeys = HM.mapWithKey- (\k v -> if keys k then v + 1 else 0)- ( maybe- id- (\specified m -> HM.fromList $ map (\s -> (s, m HM.! s)) specified)- (watchKeys conf)- $ keyStates loopState- )- let loopState' = loopState { keyStates = specifiedKeys }+ forM_+ ( catMaybes $ map+ ( \e -> case e of+ NotifyEvent ev -> Just ev+ _ -> Nothing+ )+ events+ )+ $ \ev -> patchAppConfig+ (fromJust $ appConfigFile conf)+ (componentResolver conf)++ let loop' =+ loop+ & _keyStates+ %~ HM.mapWithKey (\k v -> if keys k then v + 1 else 0)+ . maybe+ id+ ( \specified m -> HM.fromList $ map+ (\s -> (s, if HM.member s m then m HM.! s else 0))+ specified+ )+ (watchKeys conf)+ let quit = any ( \event -> case SDL.eventPayload event of SDL.WindowClosedEvent _ -> True@@ -208,16 +241,4 @@ ) events - unless quit $ go loopState' s'------withSDL :: (MonadIO m, MonadMask m) => m a -> m a-withSDL =- bracket (SDL.initializeAll >> SDL.Font.initialize)- (\_ -> SDL.Font.quit >> SDL.quit)- . const--withWindow :: (MonadIO m, MonadMask m) => (SDL.Window -> m a) -> m a-withWindow =- bracket (SDL.createWindow "window" SDL.defaultWindow) SDL.destroyWindow+ unless quit $ go loop' loader s'
src/MiniLight/Component.hs view
@@ -40,16 +40,8 @@ -} module MiniLight.Component ( module MiniLight.Component.Types,- loadAppConfig,-- Resolver,+ module MiniLight.Component.Loader, ) where -import qualified Data.Aeson as Aeson-import qualified Data.Text as T-import MiniLight.Light import MiniLight.Component.Types import MiniLight.Component.Loader--type Resolver = T.Text -> Aeson.Value -> MiniLight Component-
+ src/MiniLight/Component/Internal/Resolver.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ViewPatterns #-}+module MiniLight.Component.Internal.Resolver where++import Control.Applicative+import Data.Aeson hiding (Result)+import qualified Data.HashMap.Strict as HM+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Scientific (Scientific, fromFloatDigits)+import qualified Data.Vector as V+import Text.Trifecta++data Expr+ = None+ | Ref T.Text -- ^ reference syntax: ${ref:...}+ | Var T.Text -- ^ variable syntax: ${var:...}+ | Op T.Text Expr Expr -- ^ expr operator: +, -, *, /+ | Constant Value -- ^ constants (string or number or null)+ deriving (Eq, Show)++parser :: Parser Expr+parser = try reference <|> try variable <|> (char '$' *> braces (number <|> expr))+ where+ expr = chainl expr1 op1 None+ expr1 = chainl expr2 op2 None+ expr2 = parens expr <|> try reference <|> try variable <|> try number++ -- low precedence infixl operator group+ op1 = Op "+" <$ textSymbol "+" <|> Op "-" <$ textSymbol "-"++ -- high precedence infixl operator group+ op2 = Op "*" <$ textSymbol "*" <|> Op "/" <$ textSymbol "/"++ reference = do+ char '$'+ braces $ text "ref:" *> (fmap (Ref . T.pack) (many (letter <|> oneOf ".")))+ variable = do+ char '$'+ braces $ text "var:" *> (fmap (Var . T.pack) (many (letter <|> oneOf ".")))+ number = fmap (Constant . Number . either fromIntegral fromFloatDigits)+ integerOrDouble++data Context = Context {+ path :: V.Vector (Either Int T.Text),+ variables :: Object,+ target :: Value+}++getAt :: Value -> [Either Int T.Text] -> Value+getAt = go+ where+ go value [] = value+ go (Object obj) (Right key:ps) | key `HM.member` obj = go (obj HM.! key) ps+ go (Array arr) (Left i :ps) | 0 <= i && i < V.length arr = go (arr V.! i) ps+ go v (p:_) =+ error+ $ "TypeError: path `"+ <> show p+ <> "` is missing in `"+ <> show v+ <> "`"++normalize+ :: V.Vector (Either Int T.Text) -> [Either Int T.Text] -> [Either Int T.Text]+normalize path1 ts = V.toList path1' ++ dropWhile (\v -> v == Right "") ts+ where+ depth = length $ takeWhile (\v -> v == Right "") ts+ path1' = V.take (V.length path1 - depth - 1) path1++pattern Arithmetic :: T.Text -> Scientific -> Scientific -> Expr+pattern Arithmetic op n1 n2 =+ Op op (Constant (Number n1)) (Constant (Number n2))++eval :: Context -> Expr -> Value+eval ctx = go+ where+ go None = ""+ go (Ref path') =+ getAt (target ctx) (normalize (path ctx) (convertPath path'))+ go (Var path') =+ getAt (Object (variables ctx)) (normalize V.empty (convertPath path'))+ go (binds -> Arithmetic "+" n1 n2) = Number (n1+n2)+ go (binds -> Arithmetic "-" n1 n2) = Number (n1-n2)+ go (binds -> Arithmetic "*" n1 n2) = Number (n1*n2)+ go (binds -> Arithmetic "/" n1 n2) = Number (n1/n2)+ go expr = error $ "Illegal expression: " ++ show expr++ binds (Op op e1 e2) = Op op (Constant (eval ctx e1)) (Constant (eval ctx e2))+ binds _ = undefined++convertPath :: T.Text -> [Either Int T.Text]+convertPath+ = map+ ( \t ->+ foldResult (\_ -> Right t) (Left . fromIntegral) $ parseText index t+ )+ . T.splitOn "."+ . (\t -> if T.length t > 0 && T.head t == '.' then T.tail t else t)+ where index = char '[' *> natural <* char ']'++convert :: Context -> T.Text -> Value+convert ctx t = foldResult (\_ -> String t) (eval ctx) $ parseText parser t++parseText :: Parser a -> T.Text -> Result a+parseText parser = parseByteString parser mempty . TE.encodeUtf8++resolve :: Value -> Value+resolve = \value -> go (Context V.empty HM.empty value) value+ where+ go ctx (Object obj)+ | "_vars" `HM.member` obj+ = let vars = obj HM.! "_vars"+ in go+ ( ctx+ { variables = HM.union ((\(Object o) -> o) $ vars) (variables ctx)+ }+ )+ (Object (HM.delete "_vars" obj))+ | otherwise+ = Object $ HM.mapWithKey+ (\key -> go (ctx { path = V.snoc (path ctx) (Right key) }))+ obj+ go ctx (Array arr) =+ Array $ V.imap (\i -> go (ctx { path = V.snoc (path ctx) (Left i) })) arr+ go ctx (String t) = convert ctx t+ go _ (Number n) = Number n+ go _ (Bool b) = Bool b+ go _ Null = Null
+ src/MiniLight/Component/Internal/Types.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE DeriveGeneric #-}+module MiniLight.Component.Internal.Types where++import Data.Aeson+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.UUID+import qualified Data.UUID.V4+import GHC.Generics+import MiniLight.Component.Types+import MiniLight.Light++data ComponentConfig = ComponentConfig {+ name :: T.Text,+ properties :: Value+} deriving (Eq, Show, Generic)++instance ToJSON ComponentConfig+instance FromJSON ComponentConfig++data AppConfig = AppConfig {+ app :: V.Vector ComponentConfig,+ uuid :: V.Vector T.Text+} deriving (Eq, Show)++instance FromJSON AppConfig where+ parseJSON = withObject "app" $ \v -> do+ app <- v .: "app"++ return $ AppConfig app V.empty++-- | The type for component resolver+type Resolver+ = T.Text -- ^ Component Type+ -> T.Text -- ^ UID+ -> Value -- ^ Component Property+ -> MiniLight (Either String Component)++-- | Generate an unique id.+newUID :: MonadIO m => m T.Text+newUID = liftIO $ Data.UUID.toText <$> Data.UUID.V4.nextRandom++-- | Create a component with given resolver.+createComponentBy+ :: Resolver+ -> Maybe T.Text+ -> ComponentConfig+ -> MiniLight (Either String Component)+createComponentBy resolver uid config =+ maybe newUID return uid >>= \u -> resolver (name config) u (properties config)
src/MiniLight/Component/Loader.hs view
@@ -1,158 +1,217 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE ViewPatterns #-}-module MiniLight.Component.Loader where+module MiniLight.Component.Loader (+ module MiniLight.Component.Internal.Types, -import Control.Applicative-import Control.Monad.IO.Class-import Data.Aeson hiding (Result)-import qualified Data.HashMap.Strict as HM-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE+ LoaderEnv (..),+ HasLoaderEnv (..),++ resolveConfig,+ loadAppConfig,+ patchAppConfig,+) where++import Control.Lens+import Control.Monad+import qualified Control.Monad.Caster as Caster+import Control.Monad.Catch+import Control.Monad.Trans+import Control.Monad.Trans.Maybe+import Data.Aeson+import qualified Data.Aeson.Diff as Diff+import Data.Aeson.Patch+import Data.Aeson.Pointer+import Data.IORef+import qualified Data.Registry as R import qualified Data.Vector as V-import Data.Scientific (Scientific, fromFloatDigits) import Data.Yaml (decodeFileEither)-import GHC.Generics import MiniLight.Light import MiniLight.Component.Types-import Text.Trifecta+import MiniLight.Component.Internal.Types+import MiniLight.Component.Internal.Resolver (resolve) -data ComponentConfig = ComponentConfig {- name :: T.Text,- properties :: Value-} deriving Generic+-- | The environment for config loader+data LoaderEnv = LoaderEnv {+ registry :: R.Registry Component,+ appConfig :: IORef AppConfig+} -instance FromJSON ComponentConfig+makeClassy_ ''LoaderEnv -data AppConfig = AppConfig {- app :: [ComponentConfig]-} deriving Generic+toEither :: Result a -> Either String a+toEither (Error s) = Left s+toEither (Success a) = Right a -instance FromJSON AppConfig+-- | Load an config file and return the resolved @AppConfig@.+resolveConfig :: MonadIO m => FilePath -> m (Either String AppConfig)+resolveConfig path =+ liftIO+ $ (toEither . fromJSON <=< fmap resolve)+ . either (Left . show) Right+ <$> decodeFileEither path --- | Load an config file and construct components.+-- | Load an config file and set in the environment. Calling this function at once, this overrides all values in the environment.+-- This will generate an error log and skip the component if the configuration is invalid.+-- This function also assign unique IDs for each component, using 'assignUID'. loadAppConfig- :: (HasLightEnv env, MonadIO m)+ :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m) => FilePath -- ^ Filepath to the yaml file.- -> (T.Text -> Value -> LightT env m Component) -- ^ Specify any resolver.- -> LightT env m [Component]-loadAppConfig path mapper = do- conf <- liftIO $ (\(Data.Aeson.Success a) -> a) . fromJSON . resolve . either (error . show) id <$> decodeFileEither path- mapM (\conf -> mapper (name conf) (properties conf)) (app conf)+ -> Resolver -- ^ Specify any resolver.+ -> LightT env m ()+loadAppConfig path mapper = fmap (maybe () id) $ runMaybeT $ do+ conf <- resolveConfig path >>= \case+ Left e -> do+ lift $ Caster.err e+ fail ""+ Right r -> return r -data Expr- = None- | Ref T.Text -- ^ reference syntax: ${ref:...}- | Var T.Text -- ^ variable syntax: ${var:...}- | Op T.Text Expr Expr -- ^ expr operator: +, -, *, /- | Constant Value -- ^ constants (string or number or null)- deriving (Eq, Show)+ confs <- lift $ fmap (V.mapMaybe id) $ V.forM (app conf) $ \conf -> do+ uid <- newUID+ result <- liftMiniLight $ mapper (name conf) uid (properties conf) -parser :: Parser Expr-parser = try reference <|> try variable <|> (char '$' *> braces (number <|> expr))- where- expr = chainl expr1 op1 None- expr1 = chainl expr2 op2 None- expr2 = parens expr <|> try reference <|> try variable <|> try number+ case result of+ Left e -> do+ Caster.err e+ return Nothing+ Right component -> do+ reg <- view _registry+ R.register reg uid component - -- low precedence infixl operator group- op1 = Op "+" <$ textSymbol "+" <|> Op "-" <$ textSymbol "-"+ Caster.info+ $ "Component loaded: {name: "+ <> show (name conf)+ <> ", uid: "+ <> show uid+ <> "}" - -- high precedence infixl operator group- op2 = Op "*" <$ textSymbol "*" <|> Op "/" <$ textSymbol "/"+ return $ Just (uid, conf) - reference = do- char '$'- braces $ text "ref:" *> (fmap (Ref . T.pack) (many (letter <|> oneOf ".")))- variable = do- char '$'- braces $ text "var:" *> (fmap (Var . T.pack) (many (letter <|> oneOf ".")))- number = fmap (Constant . Number . either fromIntegral fromFloatDigits)- integerOrDouble+ ref <- view _appConfig+ liftIO $ writeIORef ref $ AppConfig (V.map snd confs) (V.map fst confs) -data Context = Context {- path :: V.Vector (Either Int T.Text),- variables :: Object,- target :: Value-}+-- | Load the config file again and place the newly loaded components. This is used for HCR (hot component replacement).+-- Call 'loadAppConfig' first.+patchAppConfig+ :: (HasLightEnv env, HasLoaderEnv env, MonadIO m, MonadCatch m)+ => FilePath -- ^ Filepath to the yaml file.+ -> Resolver -- ^ Specify any resolver.+ -> LightT env m ()+patchAppConfig path resolver = fmap (maybe () id) $ runMaybeT $ do+ cref <- view _appConfig+ appConfig <- liftIO $ readIORef cref -getAt :: Value -> [Either Int T.Text] -> Value-getAt = go- where- go value [] = value- go (Object obj) (Right key:ps) | key `HM.member` obj = go (obj HM.! key) ps- go (Array arr) (Left i :ps) | 0 <= i && i < V.length arr = go (arr V.! i) ps- go v (p:_) =- error- $ "TypeError: path `"- <> show p- <> "` is missing in `"- <> show v- <> "`"+ conf' <- do+ mconf <- lift $ resolveConfig path -normalize- :: V.Vector (Either Int T.Text) -> [Either Int T.Text] -> [Either Int T.Text]-normalize path1 ts = V.toList path1' ++ dropWhile (\v -> v == Right "") ts- where- depth = length $ takeWhile (\v -> v == Right "") ts- path1' = V.take (V.length path1 - depth - 1) path1+ case mconf of+ Left e -> do+ lift $ Caster.err e+ fail ""+ Right r -> return r -pattern Arithmetic :: T.Text -> Scientific -> Scientific -> Expr-pattern Arithmetic op n1 n2 =- Op op (Constant (Number n1)) (Constant (Number n2))+ lift+ $ forM_+ ( Diff.patchOperations+ $ Diff.diff (toJSON $ app appConfig) (toJSON $ app conf')+ )+ $ \op -> fmap (maybe () id) $ runMaybeT $ do+ lift $ Caster.debug $ "CMR detected: " <> show op -eval :: Context -> Expr -> Value-eval ctx = go+ case op of+ Add (Pointer [AKey n]) v -> create n v+ Rem (Pointer [AKey n]) -> remove n+ Rep (Pointer [AKey n ]) v -> modify n (Rep (Pointer []) v)+ Rep (Pointer (AKey n:path)) v -> modify n (Rep (Pointer path) v)+ Add (Pointer (AKey n:path)) v -> modify n (Add (Pointer path) v)+ Rem (Pointer (AKey n:path)) -> modify n (Rem (Pointer path))+ _ ->+ lift+ $ Caster.warn+ $ "CMR does not support the operation yet: "+ <> show op where- go None = ""- go (Ref path') =- getAt (target ctx) (normalize (path ctx) (convertPath path'))- go (Var path') =- getAt (Object (variables ctx)) (normalize V.empty (convertPath path'))- go (binds -> Arithmetic "+" n1 n2) = Number (n1+n2)- go (binds -> Arithmetic "-" n1 n2) = Number (n1-n2)- go (binds -> Arithmetic "*" n1 n2) = Number (n1*n2)- go (binds -> Arithmetic "/" n1 n2) = Number (n1/n2)- go expr = error $ "Illegal expression: " ++ show expr+ create n v = do+ cref <- view _appConfig+ compConf <- case fromJSON v of+ Success a -> return a+ Error err -> do+ lift $ Caster.err err+ fail "" - binds (Op op e1 e2) = Op op (Constant (eval ctx e1)) (Constant (eval ctx e2))- binds _ = undefined+ newID <- lift newUID+ component <- do+ result <- lift $ liftMiniLight $ createComponentBy resolver+ (Just newID)+ compConf -convertPath :: T.Text -> [Either Int T.Text]-convertPath- = map- ( \t ->- foldResult (\_ -> Right t) (Left . fromIntegral) $ parseText index t- )- . T.splitOn "."- . (\t -> if T.length t > 0 && T.head t == '.' then T.tail t else t)- where index = char '[' *> natural <* char ']'+ case result of+ Left err -> do+ lift $ Caster.err $ "Failed to resolve: " <> err+ fail ""+ Right c -> return c -convert :: Context -> T.Text -> Value-convert ctx t = foldResult (\_ -> String t) (eval ctx) $ parseText parser t+ reg <- view _registry+ lift $ R.insert reg n (getUID component) component -parseText :: Parser a -> T.Text -> Result a-parseText parser = parseByteString parser mempty . TE.encodeUtf8+ lift+ $ Caster.info+ $ "Component registered: {name: "+ <> show (name compConf)+ <> ", uid: "+ <> show (getUID component)+ <> "}" -resolve :: Value -> Value-resolve = \value -> go (Context V.empty HM.empty value) value- where- go ctx (Object obj)- | "_vars" `HM.member` obj- = let vars = obj HM.! "_vars"- in go- ( ctx- { variables = HM.union ((\(Object o) -> o) $ vars) (variables ctx)- }- )- (Object (HM.delete "_vars" obj))- | otherwise- = Object $ HM.mapWithKey- (\key -> go (ctx { path = V.snoc (path ctx) (Right key) }))- obj- go ctx (Array arr) =- Array $ V.imap (\i -> go (ctx { path = V.snoc (path ctx) (Left i) })) arr- go ctx (String t) = convert ctx t- go _ (Number n) = Number n- go _ (Bool b) = Bool b- go _ Null = Null+ liftIO $ modifyIORef' cref $ \conf -> conf+ { app = V.snoc (app conf) compConf+ , uuid = V.snoc (uuid conf) newID+ }++ remove n = do+ cref <- view _appConfig+ appConf <- liftIO $ readIORef cref++ let uid = uuid appConf V.! n++ reg <- view _registry+ lift $ R.delete reg uid+ lift $ Caster.info $ "Component deleted: {uid: " <> show uid <> "}"++ liftIO $ writeIORef cref $ appConf+ { app = V.ifilter (\i _ -> i /= n) $ app appConf+ , uuid = V.ifilter (\i _ -> i /= n) $ uuid appConf+ }++ modify n op = do+ cref <- view _appConfig+ appConf <- liftIO $ readIORef cref++ compConf <-+ case Diff.applyOperation op (toJSON (app appConf V.! n)) >>= fromJSON of+ Success a -> return a+ Error err -> do+ lift $ Caster.err err+ fail ""++ let uid = uuid appConf V.! n++ component <- do+ result <- lift $ liftMiniLight $ createComponentBy resolver+ (Just uid)+ compConf++ case result of+ Left err -> do+ lift $ Caster.err $ "Failed to resolve: " <> err+ fail ""+ Right c -> return c++ reg <- view _registry+ lift $ R.write reg uid component+ lift+ $ Caster.info+ $ "Component replaced: {name: "+ <> show (name compConf)+ <> ", uid: "+ <> show (getUID component)+ <> "}"++ liftIO $ writeIORef cref $ appConf { app = app appConf V.// [(n, compConf)]+ }
src/MiniLight/Component/Types.hs view
@@ -11,14 +11,11 @@ propagate, ) where +import Control.Lens import Control.Monad.Catch import Control.Monad.IO.Class import Data.IORef-import qualified Data.UUID-import qualified Data.UUID.V4 import qualified Data.Text as T-import Lens.Micro-import Lens.Micro.Mtl import MiniLight.Light import MiniLight.Event import MiniLight.Figure@@ -35,7 +32,7 @@ -> LightT env m () emit et = do uid <- view uidL- ref <- view signalQueueL+ ref <- view _signalQueue liftIO $ modifyIORef' ref $ (signal uid et :) -- | CompoonentUnit typeclass provides a way to define a new component.@@ -82,10 +79,10 @@ -- | Create a new component. newComponent :: (ComponentUnit c, HasLightEnv env, MonadIO m, MonadMask m)- => c+ => T.Text+ -> c -> LightT env m Component-newComponent c = do- uid <- liftIO $ Data.UUID.toText <$> Data.UUID.V4.nextRandom+newComponent uid c = do figs <- figures c ref <- liftIO $ newIORef figs return $ Component {uid = uid, component = c, prev = c, cache = ref}
src/MiniLight/Event.hs view
@@ -13,6 +13,7 @@ import qualified Data.Text as T import Data.Type.Equality import Type.Reflection+import qualified System.FSNotify as Notify -- | EventType says some type can be used as an event type. class Typeable e => EventType e@@ -34,6 +35,7 @@ = Never | Signal T.Text Dynamic | RawEvent SDL.Event+ | NotifyEvent Notify.Event signal :: EventType a => T.Text -> a -> Event signal t v = Signal t (toDyn v)
src/MiniLight/Figure.hs view
@@ -5,12 +5,11 @@ -- | This module provides many convenient operations for textures. module MiniLight.Figure where +import Control.Lens import Control.Monad.Catch import Control.Monad.IO.Class import qualified Data.Text as T import Data.Word (Word8)-import Lens.Micro-import Lens.Micro.Mtl import Linear (_x, _y) import MiniLight.Light import qualified SDL@@ -55,7 +54,7 @@ -- | Render a figure. render :: (HasLightEnv env, MonadIO m, MonadMask m) => Figure -> LightT env m () render fig = do- renderer <- view rendererL+ renderer <- view _renderer SDL.copyEx renderer (texture fig)@@ -140,7 +139,7 @@ {-# INLINE rotate #-} text font color txt = do- renderer <- view rendererL+ renderer <- view _renderer withBlendedText font txt color $ \surf -> do texture <- SDL.createTextureFromSurface renderer surf@@ -151,7 +150,7 @@ {-# INLINE text #-} picture filepath = do- renderer <- view rendererL+ renderer <- view _renderer texture <- SDL.Image.loadTexture renderer filepath tinfo <- SDL.queryTexture texture@@ -168,7 +167,7 @@ {-# INLINE fromTexture #-} rectangleOutline color size = do- rend <- view rendererL+ rend <- view _renderer tex <- SDL.createTexture rend SDL.RGBA8888 SDL.TextureAccessTarget (fmap toEnum size) SDL.textureBlendMode tex SDL.$= SDL.BlendAlphaBlend @@ -181,7 +180,7 @@ {-# INLINE rectangleOutline #-} rectangleFilled color size = do- rend <- view rendererL+ rend <- view _renderer tex <- SDL.createTexture rend SDL.RGBA8888 SDL.TextureAccessTarget (fmap toEnum size) SDL.textureBlendMode tex SDL.$= SDL.BlendAlphaBlend @@ -194,7 +193,7 @@ {-# INLINE rectangleFilled #-} triangleOutline color size = do- rend <- view rendererL+ rend <- view _renderer tex <- SDL.createTexture rend SDL.RGBA8888 SDL.TextureAccessTarget (fmap toEnum size) SDL.textureBlendMode tex SDL.$= SDL.BlendAlphaBlend
src/MiniLight/Light.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE RoleAnnotations #-}+{-# LANGUAGE UndecidableInstances #-} module MiniLight.Light ( HasLightEnv (..), LightT (..),@@ -10,6 +12,7 @@ mapLightT, HasLoopEnv (..),+ LoopEnv (..), FontDescriptor(..), FontStyle(..),@@ -21,28 +24,44 @@ MonadIO(..), ) where +import Control.Concurrent.MVar+import Control.Lens import Control.Monad.IO.Class+import qualified Control.Monad.Caster as Caster import Control.Monad.Catch import Control.Monad.Reader import Data.Hashable (Hashable(..)) import qualified Data.HashMap.Strict as HM import Data.IORef import Graphics.Text.TrueType-import Lens.Micro-import Lens.Micro.Mtl import MiniLight.Event import qualified SDL import qualified SDL.Font -type FontMap = HM.HashMap FontDescriptor FilePath- instance Hashable FontDescriptor where hashWithSalt n fd = let style = _descriptorStyle fd in hashWithSalt n (_descriptorFamilyName fd, _fontStyleBold style, _fontStyleItalic style) -class HasLightEnv env where- rendererL :: Lens' env SDL.Renderer- fontCacheL :: Lens' env FontMap +type FontMap = HM.HashMap FontDescriptor FilePath++-- | The environment for LightT monad.+data LightEnv = LightEnv+ { renderer :: SDL.Renderer -- ^ Renderer for SDL2+ , fontCache :: FontMap -- ^ System font information+ , logger :: Caster.LogQueue -- ^ Logger connected stdout+ }++makeClassy_ ''LightEnv++data LoopEnv = LoopEnv+ { keyStates :: HM.HashMap SDL.Scancode Int -- ^ Current state of keys, represents how many frames the key down has been down+ , events :: MVar [Event] -- ^ Event queue+ , signalQueue :: IORef [Event] -- ^ Signals emitted from components are stored in the queue and poll in the next frame.+ }++makeClassy_ ''LoopEnv++ newtype LightT env m a = LightT { runLightT' :: ReaderT env m a } deriving (Functor, Applicative, Monad, MonadIO, MonadThrow, MonadMask, MonadCatch) @@ -50,25 +69,16 @@ ask = LightT ask local f = LightT . local f . runLightT' -data LightEnv = LightEnv- { renderer :: SDL.Renderer- , fontCache :: FontMap- }--instance HasLightEnv LightEnv where- rendererL = lens renderer (\env r -> env { renderer = r })- fontCacheL = lens fontCache (\env r -> env { fontCache = r })+instance (Monad m, HasLightEnv env) => Caster.MonadLogger (LightT env m) where+ getLogger = view _logger type MiniLight = LightT LightEnv IO liftMiniLight :: (HasLightEnv env, MonadIO m) => MiniLight a -> LightT env m a liftMiniLight m = do- renderer <- view rendererL- fontCache <- view fontCacheL+ env <- view lightEnv - LightT $ ReaderT $ \_ -> liftIO $ runReaderT- (runLightT' m)- (LightEnv {renderer = renderer, fontCache = fontCache})+ LightT $ ReaderT $ \_ -> liftIO $ runReaderT (runLightT' m) env {-# INLINE liftMiniLight #-} envLightT :: (env' -> env) -> LightT env m a -> LightT env' m a@@ -79,16 +89,6 @@ mapLightT f m = LightT $ ReaderT $ f . runReaderT (runLightT' m) {-# INLINE mapLightT #-} -class HasLoopEnv env where- -- | Contains the number of frames that a specific keys are continuously pressing.- keyStatesL :: Lens' env (HM.HashMap SDL.Scancode Int)-- -- | Occurred events since the last frame.- eventsL :: Lens' env (IORef [Event])-- -- | A queue storing the events occurred in this frame.- signalQueueL :: Lens' env (IORef [Event])- loadFontCache :: MonadIO m => m FontMap loadFontCache = do fc <- liftIO buildCache@@ -107,7 +107,7 @@ -> Int -> LightT env m SDL.Font.Font loadFont fd size = do- fc <- view fontCacheL+ fc <- view _fontCache let path = fc HM.! fd SDL.Font.load path size