diff --git a/package.json b/package.json index 1b878e0..5892172 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "scripts": { "prepare:purescript": "rm -rf output/bundle.js && hygen bundle prepare", "build:purescript": "spago build", - "dev": "pnpm run prepare:purescript && parcel public/index.html --port 8080 & nodemon", + "dev": "pnpm run prepare:purescript && parcel public/index.html --port 8080", "prebuild": "rm -rf dist", "bundle:purescript": "cross-env NODE_ENV=production pnpm run prepare:purescript && spago bundle-app -t output/prod-bundle.js", "build": "tsc && pnpm run bundle:purescript && parcel build public/index.html", diff --git a/public/styles/components/node.scss b/public/styles/components/node.scss index 3dd7c1d..735349e 100644 --- a/public/styles/components/node.scss +++ b/public/styles/components/node.scss @@ -1,3 +1,21 @@ +@import "../theme.scss"; + .node.selected { border: 10px solid red; } + +.node-input { + transition: stroke-width $transition-time; +} + +.node-input:hover { + stroke-width: 9px; +} + +.node-output { + transition: transform $transition-time; +} + +.node-output:hover { + transform: scale(1.3); +} diff --git a/public/styles/pages/editor/add.scss b/public/styles/pages/editor/add.scss index 60ce1d6..8b76fac 100644 --- a/public/styles/pages/editor/add.scss +++ b/public/styles/pages/editor/add.scss @@ -61,6 +61,10 @@ overflow: hidden; text-transform: capitalize; } + + #node-type { + font-size: 0.85rem; + } } } } diff --git a/public/styles/theme.scss b/public/styles/theme.scss index 79c5ffe..39392a1 100644 --- a/public/styles/theme.scss +++ b/public/styles/theme.scss @@ -1,5 +1,5 @@ -// I curently have only one theme so using hardcoded scss variables is ok -// In the future I might take a more complex aproach if I want to allow multiple themes +// I currently have only one theme so using hard-coded scss variables is ok +// In the future I might take a more complex approach if I want to allow multiple themes $primary: #262335; $on-primary: #e2e2e2; diff --git a/spago.dhall b/spago.dhall index 369c1dc..9e29654 100644 --- a/spago.dhall +++ b/spago.dhall @@ -9,11 +9,13 @@ You can edit this file as you like. , "arrays" , "colehaus-graphs" , "console" + , "css" , "data-default" , "debug" , "effect" , "generics-rep" , "halogen" + , "halogen-css" , "halogen-svg" , "halogen-vdom" , "lists" @@ -29,6 +31,7 @@ You can edit this file as you like. , "routing-duplex" , "sized-vectors" , "spec" + , "stringutils" , "tuples" , "typelevel" , "typelevel-prelude" diff --git a/src/Component/Editor.purs b/src/Component/Editor.purs index 0795b51..cdb9144 100644 --- a/src/Component/Editor.purs +++ b/src/Component/Editor.purs @@ -6,7 +6,6 @@ import Control.Monad.State (get, gets, modify_, put) import Control.MonadZero (guard) import Data.Array (foldr, (..)) import Data.Default (def) -import Data.Either (Either(..)) import Data.Foldable (for_, sequence_) import Data.Lens (over, preview, set, view) import Data.List.Lazy as List @@ -28,9 +27,10 @@ import Lunarbox.Component.Editor.Tree as TreeC import Lunarbox.Component.Icon (icon) import Lunarbox.Component.Utils (container) import Lunarbox.Config (Config) -import Lunarbox.Control.Monad.Dataflow.Solve.SolveExpression (printTypeMap, solveExpression) +import Lunarbox.Control.Monad.Dataflow.Solve.SolveExpression (printTypeMap) import Lunarbox.Control.Monad.Effect (print, printString) import Lunarbox.Data.Dataflow.Class.Expressible (nullExpr) +import Lunarbox.Data.Dataflow.Expression (printSource) import Lunarbox.Data.Dataflow.Native.Prelude (loadPrelude) import Lunarbox.Data.Dataflow.Type (numberOfInputs) import Lunarbox.Data.Editor.ExtendedLocation (ExtendedLocation(..)) @@ -40,13 +40,12 @@ import Lunarbox.Data.Editor.Node.NodeData (NodeData(..), _NodeDataPosition, _Nod import Lunarbox.Data.Editor.Node.NodeDescriptor (onlyEditable) import Lunarbox.Data.Editor.Node.NodeId (NodeId(..)) import Lunarbox.Data.Editor.Node.PinLocation (Pin(..)) -import Lunarbox.Data.Editor.Project (_projectNodeGroup, compileProject, createFunction, emptyProject) -import Lunarbox.Data.Editor.State (State, Tab(..), _atColorMap, _atNode, _atNodeData, _currentFunction, _currentTab, _function, _functions, _isSelected, _lastMousePosition, _nextId, _nodeData, _panelIsOpen, _project, _typeMap, tabIcon) +import Lunarbox.Data.Editor.Project (_projectNodeGroup, createFunction, emptyProject) +import Lunarbox.Data.Editor.State (State, Tab(..), _atColorMap, _atCurrentNode, _atNode, _atNodeData, _currentFunction, _currentTab, _expression, _function, _functions, _isSelected, _lastMousePosition, _nextId, _nodeData, _panelIsOpen, _partialFrom, _partialTo, _project, _typeMap, compile, tabIcon, tryConnecting) import Lunarbox.Data.Graph as G import Lunarbox.Data.Vector (Vec2) import Lunarbox.Page.Editor.EmptyEditor (emptyEditor) import Lunarbox.Svg.Attributes (transparent) -import Record as Record data Action = ChangeTab Tab @@ -54,10 +53,11 @@ data Action | SelectFunction (Maybe FunctionName) | CreateNode FunctionName | StartFunctionCreation - | Compile | SceneMouseUp | SceneMouseDown (Vec2 Number) | SceneMouseMove (Vec2 Number) + | SelectInput NodeId Int + | SelectOutput NodeId | SelectNode NodeId | LoadNodes @@ -90,6 +90,7 @@ component = , lastMousePosition: Nothing , expression: nullExpr Nowhere , project: emptyProject $ NodeId "firstOutput" + , partialConnection: def } , render , eval: @@ -103,22 +104,7 @@ component = handleAction :: Action -> HalogenM State Action ChildSlots Void m Unit handleAction = case _ of LoadNodes -> do - modify_ loadPrelude - handleAction Compile - Compile -> do - { project, expression } <- get - let - expression' = compileProject project - -- we only run the type inference algorithm if the expression changed - when (expression /= expression') do - let - typeMap = case solveExpression expression' of - Right map -> Map.delete Nowhere map - Left _ -> mempty - printString $ printTypeMap typeMap - -- printString $ printSource expression' - -- TODO: make it so this accounts for errors - modify_ $ Record.merge { expression: expression', typeMap } + modify_ $ compile <<< loadPrelude CreateNode name -> do print "here:)" Tuple id setId <- createId @@ -152,8 +138,9 @@ component = state'' = set (_atNode currentFunction id) (Just node) state' state''' = set (_atNodeData currentFunction id) (Just def) state'' - void $ put $ setId state''' - handleAction Compile + + state'''' = over _functions (G.insertEdge name currentFunction) state''' + void $ put $ compile $ setId state'''' ChangeTab newTab -> do oldTab <- gets $ view _currentTab modify_ @@ -179,9 +166,8 @@ component = function <- G.lookup currentFunction functions pure do - handleAction Compile -- And finally, save the selected function in the state - modify_ $ set _currentFunction name + modify_ $ set _currentFunction name <<< compile SceneMouseDown position -> do modify_ $ set _lastMousePosition $ Just position SceneMouseMove position -> do @@ -206,6 +192,22 @@ component = maybeCurrentFunction <- gets $ view _currentFunction for_ maybeCurrentFunction \currentFunction -> do modify_ $ set (_isSelected currentFunction id) true + SelectInput id index -> do + s <- gets $ view _partialTo + let + setTo = set _partialTo $ Just $ Tuple id index + print s + modify_ $ tryConnecting <<< setTo + SelectOutput id -> do + let + setFrom = set _partialFrom $ Just id + modify_ $ tryConnecting <<< setFrom + s <- gets $ view _typeMap + e <- gets $ view _expression + s' <- gets $ preview $ _atCurrentNode $ NodeId "firstOutput" + printString $ printTypeMap s + printString $ printSource e + print s' handleTreeOutput :: TreeC.Output -> Maybe Action handleTreeOutput = case _ of @@ -302,6 +304,8 @@ component = , mouseMove: Just <<< SceneMouseMove , mouseUp: Just SceneMouseUp , selectNode: Just <<< SelectNode + , selectInput: (Just <<< _) <<< SelectInput + , selectOutput: Just <<< SelectOutput } render :: State -> HH.HTML _ Action diff --git a/src/Component/Editor/Add.purs b/src/Component/Editor/Add.purs index 02aea38..a0c4bce 100644 --- a/src/Component/Editor/Add.purs +++ b/src/Component/Editor/Add.purs @@ -15,7 +15,8 @@ import Halogen.HTML (HTML) import Halogen.HTML as HH import Halogen.HTML.Events (onClick) import Halogen.HTML.Properties as HP -import Lunarbox.Capability.Editor.Type (generateTypeMap) +import Lunarbox.Capability.Editor.Type (generateTypeMap, prettify) +import Lunarbox.Component.Editor.HighlightedType (highlightTypeToHTML) import Lunarbox.Component.Editor.Node (node) import Lunarbox.Component.Editor.Node as NodeC import Lunarbox.Component.Icon (icon) @@ -30,6 +31,7 @@ import Lunarbox.Data.Editor.Node (Node(..), hasOutput) import Lunarbox.Data.Editor.Node.NodeDescriptor (NodeDescriptor, describe) import Lunarbox.Data.Editor.Node.PinLocation (Pin(..)) import Lunarbox.Data.Editor.Project (Project) +import Svg.Attributes (Color(..)) import Svg.Attributes as SA import Svg.Elements as SE @@ -50,7 +52,7 @@ resolvePin (InputPin index) type' = inputs type' !! index resolvePin OutputPin type' = Just $ output type' -nodeInput :: Map.Map Location Type -> FunctionName -> FunctionData -> NodeC.Input +nodeInput :: forall h a. Map.Map Location Type -> FunctionName -> FunctionData -> NodeC.Input h a nodeInput typeMap name functionData = { nodeData: def , node @@ -77,13 +79,22 @@ makeNode { edit, addNode } { isUsable, isEditable } name typeMap functionData = ] [ node (nodeInput typeMap name functionData) - { select: Nothing } + { select: Nothing + , selectOutput: Nothing + , selectInput: const Nothing + } ] , container "node-data" [ container "node-text" [ container "node-name" [ HH.text $ show name ] + , container "node-type" + $ fromMaybe mempty + $ pure + <<< highlightTypeToHTML (RGB 255 255 255) + <<< prettify + <$> Map.lookup (Location name) typeMap ] , container "node-buttons" [ HH.div diff --git a/src/Component/Editor/HighlightedType.purs b/src/Component/Editor/HighlightedType.purs new file mode 100644 index 0000000..3b5715c --- /dev/null +++ b/src/Component/Editor/HighlightedType.purs @@ -0,0 +1,40 @@ +module Lunarbox.Component.Editor.HighlightedType + ( highlightedType + , highlightTypeToHTML + , highlightTypeToSvg + ) where + +import Prelude +import Data.Maybe (Maybe(..), fromMaybe) +import Halogen.HTML as HH +import Lunarbox.Capability.Editor.Type (typeToColor) +import Lunarbox.Component.HighlightedText as HT +import Lunarbox.Data.Dataflow.Type (Type(..)) +import Lunarbox.Math.SeededRandom (seededInt) +import Lunarbox.Svg.Element (tspan) +import Svg.Attributes (Color(..)) +import Svg.Attributes as SA + +-- A type which is syntax highlighted +highlightedType :: forall h a. (Array (HH.HTML h a) -> HH.HTML h a) -> (Color -> HH.HTML h a -> HH.HTML h a) -> Color -> Type -> HH.HTML h a +highlightedType container highlight defaultColor = case _ of + TArrow from to -> + container + [ highlightedType container highlight defaultColor from + , HH.text " -> " + , highlightedType container highlight defaultColor to + ] + TVarariable name' -> highlight (RGB shade shade shade) $ HH.text $ show name' + where + shade = seededInt (show name') 100 255 + other -> highlight color $ HH.text $ show other + where + color = fromMaybe defaultColor $ typeToColor other + +highlightTypeToHTML :: forall h a. Color -> Type -> HH.HTML h a +highlightTypeToHTML = highlightedType HH.span_ HT.highlight + +highlightTypeToSvg :: forall h a. Color -> Type -> HH.HTML h a +highlightTypeToSvg = + highlightedType (tspan []) \color -> + tspan [ SA.fill $ Just color ] <<< pure diff --git a/src/Component/Editor/Node.purs b/src/Component/Editor/Node.purs index ed7fab6..5aed10b 100644 --- a/src/Component/Editor/Node.purs +++ b/src/Component/Editor/Node.purs @@ -14,10 +14,9 @@ import Data.Typelevel.Num (d0, d1) import Data.Vec ((!!)) import Halogen.HTML (HTML) import Halogen.HTML as HH -import Halogen.HTML.Events (onMouseDown) +import Halogen.HTML.Events (onClick, onMouseDown) import Lunarbox.Capability.Editor.Node.NodeInput (Arc(..), fillWith) import Lunarbox.Component.Editor.Node.Input (input) -import Lunarbox.Component.Editor.Node.Label (label) import Lunarbox.Component.Editor.Node.Overlays (overlays) import Lunarbox.Data.Editor.Constants (arcSpacing, arcWidth, nodeRadius) import Lunarbox.Data.Editor.FunctionData (FunctionData(..)) @@ -30,10 +29,10 @@ import Svg.Attributes (Color) import Svg.Attributes as SA import Svg.Elements as SE -type Input +type Input h a = { nodeData :: NodeData , node :: Node - , labels :: Array String + , labels :: Array (HTML h a) , functionData :: FunctionData , colorMap :: Map Pin SA.Color , hasOutput :: Boolean @@ -41,15 +40,19 @@ type Input type Actions a = { select :: Maybe a + , selectInput :: Int -> Maybe a + , selectOutput :: Maybe a } -output :: forall r a. Boolean -> Color -> HTML r a -output false _ = HH.text "" +output :: forall r a. Boolean -> Maybe a -> Color -> HTML r a +output false _ _ = HH.text "" -output true color = +output true selectOutput color = SE.circle [ SA.r 10.0 , SA.fill $ Just color + , SA.class_ "node-output" + , onClick $ const selectOutput ] constant :: forall r a. HTML r a @@ -63,21 +66,25 @@ constant = , strokeDashArray [ pi * nodeRadius / 20.0 ] ] -node :: forall h a. Input -> Actions a -> HTML h a +node :: forall h a. Input h a -> Actions a -> HTML h a node { nodeData: NodeData { position } , functionData: FunctionData { inputs } , labels , colorMap , hasOutput -} { select } = +} { select +, selectOutput +, selectInput +} = SE.g [ SA.transform [ SA.Translate (position !! d0) (position !! d1) ] , onMouseDown $ const select ] - [ overlays $ label <$> labels + [ overlays labels , SE.circle [ SA.r nodeRadius, SA.fill $ Just transparent ] , output hasOutput + selectOutput $ fromMaybe transparent $ Map.lookup OutputPin colorMap , let @@ -92,15 +99,24 @@ node { nodeData: NodeData { position } [ SA.transform [ SA.Rotate 90.0 0.0 0.0 ] ] $ ( \arc@(Arc _ _ name) -> - input - { arc - , spacing: if List.length inputArcs == 1 then 0.0 else arcSpacing - , radius: nodeRadius - , color: - fromMaybe transparent do - index <- List.findIndex (name == _) inputNames - Map.lookup (InputPin index) colorMap - } + let + maybeIndex = List.findIndex (name == _) inputNames + in + input + { arc + , spacing: + if List.length inputArcs == 1 then + 0.0 + else + arcSpacing + , radius: nodeRadius + , color: + fromMaybe transparent do + index <- maybeIndex + Map.lookup (InputPin index) colorMap + } + $ maybeIndex + >>= selectInput ) <$> List.toUnfoldable inputArcs ] diff --git a/src/Component/Editor/Node/Input.purs b/src/Component/Editor/Node/Input.purs index 4e9ad4d..e623285 100644 --- a/src/Component/Editor/Node/Input.purs +++ b/src/Component/Editor/Node/Input.purs @@ -5,6 +5,7 @@ module Lunarbox.Component.Editor.Node.Input import Prelude import Data.Maybe (Maybe(..)) import Halogen.HTML (HTML) +import Halogen.HTML.Events (onClick) import Lunarbox.Capability.Editor.Node.NodeInput (Arc(..)) import Lunarbox.Data.Editor.Constants (arcWidth) import Lunarbox.Svg.Attributes (Linecap(..), arc, strokeLinecap, strokeWidth, transparent) @@ -19,12 +20,14 @@ type Input , color :: Color } -input :: forall h a. Input -> HTML h a -input { radius, spacing, arc: Arc start end _, color } = +input :: forall h a. Input -> Maybe a -> HTML h a +input { radius, spacing, arc: Arc start end _, color } selectInput = SE.path [ SA.d $ Abs <$> arc radius (start + spacing) (end - spacing) , SA.fill $ Just transparent , SA.stroke $ Just color + , SA.class_ "node-input" , strokeWidth arcWidth , strokeLinecap Round + , onClick $ const selectInput ] diff --git a/src/Component/Editor/Node/Label.purs b/src/Component/Editor/Node/Label.purs index 6821d45..f29dc83 100644 --- a/src/Component/Editor/Node/Label.purs +++ b/src/Component/Editor/Node/Label.purs @@ -1,5 +1,6 @@ module Lunarbox.Component.Editor.Node.Label ( label + , labelText ) where import Prelude @@ -9,10 +10,19 @@ import Halogen.HTML as HH import Svg.Attributes as SA import Svg.Elements as SE -label :: forall h a. String -> HTML h a -label text = +labelText :: forall h a. String -> HTML h a +labelText = SE.text [ SA.text_anchor SA.AnchorMiddle , SA.fill $ Just $ SA.RGB 63 196 255 ] - [ HH.text text ] + <<< pure + <<< HH.text + +label :: forall h a. HTML h a -> HTML h a +label inner = + SE.text + [ SA.text_anchor SA.AnchorMiddle + , SA.fill $ Just $ SA.RGB 63 196 255 + ] + [ inner ] diff --git a/src/Component/Editor/Scene.purs b/src/Component/Editor/Scene.purs index 757eed6..2fd3200 100644 --- a/src/Component/Editor/Scene.purs +++ b/src/Component/Editor/Scene.purs @@ -20,7 +20,9 @@ import Data.Vec (vec2) import Halogen.HTML as HH import Halogen.HTML.Events (onMouseDown, onMouseMove, onMouseUp) import Lunarbox.Capability.Editor.Type (ColoringError, generateTypeMap, prettify) +import Lunarbox.Component.Editor.HighlightedType (highlightTypeToSvg) import Lunarbox.Component.Editor.Node as NodeC +import Lunarbox.Component.Editor.Node.Label (labelText, label) import Lunarbox.Data.Dataflow.Expression (Expression, sumarizeExpression) import Lunarbox.Data.Dataflow.Expression as Expression import Lunarbox.Data.Dataflow.Type (Type) @@ -36,7 +38,7 @@ import Lunarbox.Data.Editor.Project (Project, _atProjectNode) import Lunarbox.Data.Map (maybeBimap) import Lunarbox.Data.Vector (Vec2) import Lunarbox.Page.Editor.EmptyEditor (erroredEditor) -import Svg.Attributes (Color) +import Svg.Attributes (Color(..)) import Svg.Attributes as SA import Svg.Elements as SE import Web.UIEvent.MouseEvent as ME @@ -58,6 +60,8 @@ type Actions a , mouseDown :: Vec2 Number -> Maybe a , selectNode :: NodeId -> Maybe a , mouseUp :: Maybe a + , selectInput :: NodeId -> Int -> Maybe a + , selectOutput :: NodeId -> Maybe a } -- Errors which could arise while creating the node svg @@ -89,7 +93,13 @@ getNode :: FunctionName -> NodeId -> Project -> NodeBuild Node getNode name id = note (MissingNode id) <<< join <<< (preview $ _atProjectNode name id) createNodeComponent :: forall h a. Input -> Actions a -> Tuple NodeId NodeData -> NodeBuild (HH.HTML h a) -createNodeComponent { functionName, project, typeMap, expression, functionData, typeColors } { selectNode } (Tuple id nodeData) = do +createNodeComponent { functionName +, project +, typeMap +, expression +, functionData +, typeColors +} { selectNode, selectInput, selectOutput } (Tuple id nodeData) = do let generateLocation = DeepLocation functionName @@ -118,13 +128,16 @@ createNodeComponent { functionName, project, typeMap, expression, functionData, , functionData: nodeFunctionData , colorMap , labels: - [ show name - , show $ prettify nodeType - , sumarizeExpression nodeExpression + [ labelText $ show name + , label $ highlightTypeToSvg (RGB 255 255 255) $ prettify nodeType + , labelText $ sumarizeExpression nodeExpression ] , hasOutput: not $ is _OutputNode node } - { select: selectNode id } + { select: selectNode id + , selectInput: selectInput id + , selectOutput: selectOutput id + } scene :: forall h a. Input -> Actions a -> HH.HTML h a scene state@{ project diff --git a/src/Component/HighlightedText.purs b/src/Component/HighlightedText.purs new file mode 100644 index 0000000..7baaea9 --- /dev/null +++ b/src/Component/HighlightedText.purs @@ -0,0 +1,25 @@ +module Lunarbox.Component.HighlightedText (highlight) where + +import Prelude +import CSS as CSS +import CSS as CSS.Color +import Halogen.HTML as HH +import Halogen.HTML.CSS (style) +import Svg.Attributes (Color(..)) + +highlightedClass :: String +highlightedClass = "highlighted" + +svgColorToCssColor :: Color -> CSS.Color.Color +svgColorToCssColor (RGB r g b) = CSS.Color.rgb r g b + +svgColorToCssColor (RGBA r g b a) = CSS.Color.rgba r g b a + +highlight :: forall h a. Color -> HH.HTML h a -> HH.HTML h a +highlight color inner = + HH.span + [ style + $ CSS.color + $ svgColorToCssColor color + ] + [ inner ] diff --git a/src/Control/Monad/Dataflow/Infer/InferExpression.purs b/src/Control/Monad/Dataflow/Infer/InferExpression.purs index 692d061..92e8ff9 100644 --- a/src/Control/Monad/Dataflow/Infer/InferExpression.purs +++ b/src/Control/Monad/Dataflow/Infer/InferExpression.purs @@ -7,8 +7,8 @@ import Control.Monad.Error.Class (throwError) import Control.Monad.Reader (asks, local) import Control.Monad.State (gets, modify_) import Data.Array (find, foldr, zip) -import Data.List ((:)) import Data.Lens (over, view) +import Data.List ((:)) import Data.Map as Map import Data.Maybe (Maybe(..), isJust) import Data.Set as Set @@ -97,9 +97,9 @@ infer expression = tv <- fresh createConstraint funcType (inputType `TArrow` tv) pure tv - Let _ name value body -> do + Let _ shouldGeneralize name value body -> do t <- infer value - inner <- generalize t + inner <- if shouldGeneralize then generalize t else pure $ Forall [] t createClosure name inner (infer body) If _ condition onTrue onFalse -> do conditionType <- infer condition diff --git a/src/Control/Monad/Dataflow/Solve/SolveExpression.purs b/src/Control/Monad/Dataflow/Solve/SolveExpression.purs index 5d75d40..7f2ef88 100644 --- a/src/Control/Monad/Dataflow/Solve/SolveExpression.purs +++ b/src/Control/Monad/Dataflow/Solve/SolveExpression.purs @@ -9,6 +9,7 @@ import Data.Array as Array import Data.Either (Either) import Data.Map as Map import Data.Tuple (Tuple(..)) +import Lunarbox.Capability.Editor.Type (prettify) import Lunarbox.Control.Monad.Dataflow.Infer (InferEnv(..), InferOutput(..), runInfer) import Lunarbox.Control.Monad.Dataflow.Infer.InferExpression (infer) import Lunarbox.Control.Monad.Dataflow.Solve (SolveContext(..), runSolve) @@ -38,6 +39,6 @@ solveExpression expression = do -- helper to print a typemap printTypeMap :: forall l. Show l => Ord l => Map.Map l Type -> String printTypeMap = - foldr (\(Tuple location type') result -> result <> "\n" <> show location <> " = " <> show type') "" + foldr (\(Tuple location type') result -> result <> "\n" <> show location <> " = " <> show (type')) "" <<< Array.sortBy (\(Tuple _ a) (Tuple _ b) -> compare (show a) $ show b) <<< Map.toUnfoldable diff --git a/src/Data/Dataflow/Expression.purs b/src/Data/Dataflow/Expression.purs index 28c30e2..abc8a5d 100644 --- a/src/Data/Dataflow/Expression.purs +++ b/src/Data/Dataflow/Expression.purs @@ -14,6 +14,10 @@ module Lunarbox.Data.Dataflow.Expression , sumarizeExpression , inputs , wrap + , optimize + , removeWrappers + , wrapWith + , wrappers ) where import Prelude @@ -24,6 +28,7 @@ import Data.Newtype (class Newtype, unwrap) import Data.Set (Set) import Lunarbox.Data.Dataflow.Runtime (RuntimeValue) import Lunarbox.Data.Dataflow.Scheme (Scheme) +import Lunarbox.Data.String (indent) newtype VarName = VarName String @@ -59,7 +64,7 @@ data Expression l | FunctionCall l (Expression l) (Expression l) | Lambda l VarName (Expression l) | Literal l Literal - | Let l VarName (Expression l) (Expression l) + | Let l Boolean VarName (Expression l) (Expression l) | If l (Expression l) (Expression l) (Expression l) | FixPoint l (Expression l) | Chain l (List (Expression l)) @@ -76,7 +81,7 @@ getLocation = case _ of FunctionCall l _ _ -> l Lambda l _ _ -> l Literal l _ -> l - Let l _ _ _ -> l + Let l _ _ _ _ -> l If l _ _ _ -> l FixPoint l _ -> l Native l _ -> l @@ -89,7 +94,7 @@ toMap expression = <> case expression of FunctionCall _ calee input -> toMap calee <> toMap input Lambda _ _ body -> toMap body - Let _ _ value body -> toMap value <> toMap body + Let _ _ _ value body -> toMap value <> toMap body If _ condition then' else' -> toMap condition <> toMap then' <> toMap else' Chain _ expressions -> foldr (\expression' -> (<>) $ toMap expression') mempty expressions FixPoint _ body -> toMap body @@ -139,19 +144,39 @@ printExpressionAt location = sumarizeExpression :: forall l. Show l => Eq l => Expression l -> String sumarizeExpression = printRawExpression $ const "..." +printRawLet :: forall l. (Expression l -> String) -> Expression l -> String +printRawLet print (Let _ _ name value _) = indent 2 (unwrap name <> " = " <> print value) <> "\n" + +printRawLet _ _ = "" + +printLet :: forall l. Boolean -> (Expression l -> String) -> Expression l -> String +printLet true print expression@(Let _ _ _ _ _) = "let\n" <> printLet false print expression + +printLet false print expression@(Let _ _ _ _ next@(Let _ _ _ _ _)) = printRawLet print expression <> printLet false print next + +printLet false print expression@(Let _ _ _ _ next) = printRawLet print expression <> "in\n" <> indent 2 (print next) + +printLet _ _ _ = "" + -- Prints an expression without it's location. -- Uses a custom function to print the recursive Expressions. -- Only used internally inside the show instance -- to not reepat the location printing code every time printRawExpression :: forall l. Show l => (Expression l -> String) -> Expression l -> String -printRawExpression print = case _ of +printRawExpression print expression = case expression of Variable _ name -> unwrap name FunctionCall _ f i -> print f <> " " <> print i Lambda _ arg value -> "\\" <> show arg <> " -> " <> print value Literal _ literal -> show literal - Let _ name value body -> "let " <> unwrap name <> " = " <> print value <> " in " <> print body - If _ c t f -> "if " <> print c <> " then " <> print t <> " else " <> print f + Let _ _ _ _ _ -> printLet true (printRawExpression print) expression FixPoint _ e -> "fixpoint( " <> print e <> " )" + If _ cond then' else' -> + "if\n" + <> indent 2 (print cond) + <> "\nthen\n" + <> indent 2 (print then') + <> "\nelse\n" + <> indent 2 (print else') Native _ (NativeExpression t _) -> "native :: " <> show t Chain l (e : Nil) -> printRawExpression print e Chain l (e : es) -> "{" <> printRawExpression print e <> "," <> (printRawExpression print $ Chain l es) <> "}" @@ -164,3 +189,40 @@ printSource = printRawExpression (\e -> printSource e) -- Wrap an expression in another expression with a custom location wrap :: forall l. l -> Expression l -> Expression l wrap location = Chain location <<< pure + +-- Unwrap an expression as much as possible +removeWrappers :: forall l. Expression l -> Expression l +removeWrappers (Chain _ (expression : Nil)) = removeWrappers expression + +removeWrappers expression = expression + +-- Collect all the locations something is wrapped in +wrappers :: forall l. Expression l -> List l +wrappers (Chain location (expression : Nil)) = location : wrappers expression + +wrappers _ = Nil + +-- Wrap an expression with a list of locations +wrapWith :: forall l. List l -> Expression l -> Expression l +wrapWith (location : locations') = wrapWith locations' <<< wrap location + +wrapWith Nil = identity + +-- Optimize an expression +optimize :: forall l. Expression l -> Expression l +optimize expression@(Let location generalize name value body) = case removeWrappers body of + Variable location' name' + | name == name' -> wrapWith (wrappers body) $ wrap location' $ optimize value + _ -> Let location generalize name (optimize value) $ optimize body + +optimize (FunctionCall location calee argument) = FunctionCall location (optimize calee) $ optimize argument + +optimize (Lambda location argument body) = Lambda location argument $ optimize body + +optimize (If location condition then' else') = If location (optimize condition) (optimize then') $ optimize else' + +optimize (FixPoint location body) = FixPoint location $ optimize body + +optimize (Chain location expressions) = Chain location $ optimize <$> expressions + +optimize expression = expression diff --git a/src/Data/Dataflow/Graph.purs b/src/Data/Dataflow/Graph.purs index 55cf120..5376bef 100644 --- a/src/Data/Dataflow/Graph.purs +++ b/src/Data/Dataflow/Graph.purs @@ -5,37 +5,33 @@ module Lunarbox.Data.Dataflow.Graph import Prelude import Data.Lens (view) import Data.Lens.At (at) -import Data.List (catMaybes, foldr, reverse) +import Data.List (catMaybes, foldr) import Data.Maybe (Maybe) import Data.Tuple (Tuple(..)) import Lunarbox.Data.Dataflow.Class.Expressible (nullExpr) -import Lunarbox.Data.Dataflow.Expression (Expression, VarName(..)) -import Lunarbox.Data.Editor.ExtendedLocation (ExtendedLocation(..), letWithLocation) +import Lunarbox.Data.Dataflow.Expression (Expression(..), VarName(..), wrap) +import Lunarbox.Data.Editor.ExtendedLocation (ExtendedLocation(..)) import Lunarbox.Data.Graph (Graph, topologicalSort) -- Takes a key and a graph and uses that to produce an Expression compileGraphNode :: forall k v l. Ord k => (v -> Expression l) -> Graph k v -> k -> Maybe (Tuple k (Expression (ExtendedLocation k l))) -compileGraphNode toExpression graph key = Tuple key <$> map (DeepLocation key) <$> toExpression <$> view (at key) graph +compileGraphNode toExpression graph key = Tuple key <$> wrap (Location key) <$> map (DeepLocation key) <$> toExpression <$> view (at key) graph -- Takes a graph of something and compiles it into an Expression -compileGraph :: forall k v l. Ord k => Eq l => Show k => (v -> Expression l) -> Graph k v -> Expression (ExtendedLocation k l) -compileGraph toExpression graph = +compileGraph :: forall k v l. Ord k => Eq l => Show k => Show l => (v -> Expression l) -> Graph k v -> k -> Expression (ExtendedLocation k l) +compileGraph toExpression graph main = let sorted = - reverse - $ topologicalSort - graph + topologicalSort + graph emptyExpression = nullExpr Nowhere in foldr ( \(Tuple key value) body -> - if body == emptyExpression then - value - else - letWithLocation (Location key) (VarName $ show key) value body + Let Nowhere true (VarName $ show key) value body ) - emptyExpression + (Variable Nowhere $ VarName $ show main) $ catMaybes $ compileGraphNode toExpression graph <$> sorted diff --git a/src/Data/Editor/ExtendedLocation.purs b/src/Data/Editor/ExtendedLocation.purs index d52a11d..de9c2ef 100644 --- a/src/Data/Editor/ExtendedLocation.purs +++ b/src/Data/Editor/ExtendedLocation.purs @@ -1,6 +1,5 @@ module Lunarbox.Data.Editor.ExtendedLocation ( ExtendedLocation(..) - , letWithLocation , normalize , nothing , _ExtendedLocation @@ -11,10 +10,9 @@ module Lunarbox.Data.Editor.ExtendedLocation import Prelude import Data.Default (class Default, def) import Data.Lens (Prism', prism') -import Data.List ((:), List(..)) import Data.Maybe (Maybe(..)) import Lunarbox.Data.Dataflow.Class.Expressible (nullExpr) -import Lunarbox.Data.Dataflow.Expression (Expression(..), VarName) +import Lunarbox.Data.Dataflow.Expression (Expression) -- This represents a location which may or may not have an extra or a missing layer data ExtendedLocation l l' @@ -51,21 +49,6 @@ _ExtendedLocation = DeepLocation l _ -> Just l Nowhere -> Nothing --- helpers -letWithLocation :: - forall l l'. - ExtendedLocation l l' -> - VarName -> - Expression (ExtendedLocation l l') -> - Expression (ExtendedLocation l l') -> - Expression (ExtendedLocation l l') -letWithLocation location name value body = - Let Nowhere - name - value - $ Chain Nowhere - $ (Variable location name : body : Nil) - -- Normalize nested Locations normalize :: forall l l' l''. ExtendedLocation l (ExtendedLocation l' l'') -> ExtendedLocation l (ExtendedLocation l' l'') normalize = case _ of diff --git a/src/Data/Editor/Node.purs b/src/Data/Editor/Node.purs index 58be551..8c3671c 100644 --- a/src/Data/Editor/Node.purs +++ b/src/Data/Editor/Node.purs @@ -3,18 +3,20 @@ module Lunarbox.Data.Editor.Node , ComplexNodeData , compileNode , hasOutput + , getInputs , _ComplexNodeFunction , _ComplexNodeInputs , _OutputNode + , _nodeInputs ) where import Prelude -import Data.Default (def) -import Data.Lens (Prism', Traversal', is, prism') +import Data.Lens (Lens', Prism', Traversal', is, lens, prism', set) import Data.Lens.Record (prop) -import Data.List (List, foldl, mapWithIndex) +import Data.List (List(..), foldl, mapWithIndex, (!!)) import Data.Maybe (Maybe(..), maybe) import Data.Symbol (SProxy(..)) +import Lunarbox.Data.Dataflow.Class.Expressible (nullExpr) import Lunarbox.Data.Dataflow.Expression (Expression(..), VarName(..), wrap) import Lunarbox.Data.Editor.ExtendedLocation (ExtendedLocation(..), nothing) import Lunarbox.Data.Editor.FunctionName (FunctionName) @@ -37,10 +39,22 @@ data Node ComplexNodeData | OutputNode (Maybe NodeId) +instance showNode :: Show Node where + show InputNode = "InputNode" + show (OutputNode id) = "Output " <> maybe "???" show id + show (ComplexNode data') = show data' + -- Check if a node has an output pin hasOutput :: Node -> Boolean hasOutput = not <<< is _OutputNode +-- Get all inputs of a node +getInputs :: Node -> List (Maybe NodeId) +getInputs = case _ of + ComplexNode { inputs } -> inputs + OutputNode input -> pure input + InputNode -> Nil + functionCall :: forall l l'. ExtendedLocation l l' -> Expression (ExtendedLocation l l') -> List (Expression (ExtendedLocation l l')) -> Expression (ExtendedLocation l l') functionCall location calee = wrap location <<< foldl (FunctionCall Nowhere) calee @@ -50,24 +64,27 @@ compileNode nodes id child = InputNode -> inputNode id child OutputNode outputId -> outputNode id case outputId of - Just outputId' -> Variable (Location outputId') $ VarName $ show outputId' + Just outputId' -> Variable Nowhere $ VarName $ show outputId' Nothing -> nothing - ComplexNode { inputs, function } -> Let def name value child + ComplexNode { inputs, function } -> Let Nowhere false name value child where name = VarName $ show id - calee = Variable (Location id) $ VarName $ show function + calee = Variable Nowhere $ VarName $ show function arguments = mapWithIndex ( \index id' -> - wrap (DeepLocation id $ InputPin index) case id' of - Just id'' -> Variable Nowhere $ VarName $ show id'' - Nothing -> nothing + let + location = DeepLocation id $ InputPin index + in + case id' of + Just id'' -> Variable location $ VarName $ show id'' + Nothing -> nullExpr location ) inputs - value = functionCall (DeepLocation id OutputPin) calee arguments + value = wrap (Location id) $ functionCall (DeepLocation id OutputPin) calee arguments -- Lenses _ComplexNode :: Prism' Node ComplexNodeData @@ -87,3 +104,10 @@ _OutputNode = prism' OutputNode case _ of OutputNode v -> Just v _ -> Nothing + +_nodeInputs :: Lens' Node (List (Maybe NodeId)) +_nodeInputs = + lens getInputs \node -> case node of + InputNode -> const node + OutputNode inner -> OutputNode <<< join <<< (_ !! 0) + ComplexNode _ -> flip (set _ComplexNodeInputs) node diff --git a/src/Data/Editor/NodeGroup.purs b/src/Data/Editor/NodeGroup.purs index 79301b0..0343f4c 100644 --- a/src/Data/Editor/NodeGroup.purs +++ b/src/Data/Editor/NodeGroup.purs @@ -10,7 +10,7 @@ module Lunarbox.Data.Editor.NodeGroup import Prelude import Data.Lens (Lens') import Data.Lens.Record (prop) -import Data.List (List, foldl, (\\), (:)) +import Data.List (List, foldl, (\\), (:), reverse) import Data.Newtype (class Newtype, unwrap) import Data.Symbol (SProxy(..)) import Lunarbox.Data.Dataflow.Expression (Expression, VarName(..), functionDeclaration) @@ -38,9 +38,9 @@ derive instance newtypeNodeGroup :: Newtype NodeGroup _ compileNodeGroup :: NodeGroup -> Expression NodeOrPinLocation compileNodeGroup group@(NodeGroup { nodes, output, inputs }) = let - ordered = orderNodes group + ordered = reverse $ orderNodes group - bodyNodes = output : (ordered \\ inputs) + bodyNodes = output : (ordered \\ (output : inputs)) return = foldl diff --git a/src/Data/Editor/PartialConnection.purs b/src/Data/Editor/PartialConnection.purs new file mode 100644 index 0000000..45780f9 --- /dev/null +++ b/src/Data/Editor/PartialConnection.purs @@ -0,0 +1,37 @@ +module Lunarbox.Data.Editor.PartialConnection + ( PartialConnection(..) + , _from + , _to + ) where + +import Prelude +import Data.Default (class Default) +import Data.Lens (Lens') +import Data.Lens.Record (prop) +import Data.Maybe (Maybe) +import Data.Newtype (class Newtype) +import Data.Symbol (SProxy(..)) +import Data.Tuple (Tuple) +import Lunarbox.Data.Editor.Node.NodeId (NodeId) +import Lunarbox.Data.Lens (newtypeIso) + +-- This is a data structure used to store data about the current connection the user is working on +newtype PartialConnection + = PartialConnection + { from :: Maybe NodeId + , to :: Maybe (Tuple NodeId Int) + } + +-- Typeclass instances +derive instance eqPartialConnection :: Eq PartialConnection + +derive instance newtypePartialConnection :: Newtype PartialConnection _ + +derive newtype instance defaultPartialConnection :: Default PartialConnection + +-- Lenses +_from :: Lens' PartialConnection (Maybe NodeId) +_from = newtypeIso <<< prop (SProxy :: _ "from") + +_to :: Lens' PartialConnection (Maybe (Tuple NodeId Int)) +_to = newtypeIso <<< prop (SProxy :: _ "to") diff --git a/src/Data/Editor/Project.purs b/src/Data/Editor/Project.purs index c8a22ed..e2104d4 100644 --- a/src/Data/Editor/Project.purs +++ b/src/Data/Editor/Project.purs @@ -23,10 +23,9 @@ import Data.Newtype (class Newtype) import Data.Set as Set import Data.Symbol (SProxy(..)) import Data.Unfoldable (class Unfoldable) -import Lunarbox.Data.Dataflow.Expression (Expression) +import Lunarbox.Data.Dataflow.Expression (Expression, optimize) import Lunarbox.Data.Dataflow.Graph (compileGraph) import Lunarbox.Data.Editor.DataflowFunction (DataflowFunction(..), _VisualFunction, compileDataflowFunction) -import Lunarbox.Data.Editor.ExtendedLocation (normalize) import Lunarbox.Data.Editor.FunctionName (FunctionName(..)) import Lunarbox.Data.Editor.Location (Location) import Lunarbox.Data.Editor.Node (Node(..)) @@ -50,7 +49,7 @@ _ProjectMain :: Lens' Project FunctionName _ProjectMain = newtypeIso <<< prop (SProxy :: _ "main") compileProject :: Project -> Expression Location -compileProject = map normalize <<< compileGraph compileDataflowFunction <<< view _ProjectFunctions +compileProject (Project { functions, main }) = optimize $ compileGraph compileDataflowFunction functions main createEmptyFunction :: NodeId -> DataflowFunction createEmptyFunction id = diff --git a/src/Data/Editor/State.purs b/src/Data/Editor/State.purs index 99f12fa..f2d57ae 100644 --- a/src/Data/Editor/State.purs +++ b/src/Data/Editor/State.purs @@ -2,6 +2,8 @@ module Lunarbox.Data.Editor.State ( State , Tab(..) , tabIcon + , tryConnecting + , compile , _nodeData , _atNodeData , _project @@ -21,28 +23,41 @@ module Lunarbox.Data.Editor.State , _currentTab , _functionData , _atFunctionData + , _partialConnection + , _partialFrom + , _partialTo + , _currentNodes + , _atCurrentNode + , _currentNodeGroup ) where import Prelude -import Data.Lens (Lens', Traversal', _Just) +import Data.Either (Either(..)) +import Data.Lens (Lens', Traversal', _Just, lens, over, preview, set, view) import Data.Lens.At (at) +import Data.Lens.Index (ix) import Data.Lens.Record (prop) import Data.Map (Map) -import Data.Maybe (Maybe) +import Data.Map as Map +import Data.Maybe (Maybe(..), fromMaybe) import Data.Symbol (SProxy(..)) import Data.Tuple (Tuple(..)) +import Lunarbox.Control.Monad.Dataflow.Solve.SolveExpression (solveExpression) import Lunarbox.Data.Dataflow.Expression (Expression) import Lunarbox.Data.Dataflow.Type (Type) import Lunarbox.Data.Editor.DataflowFunction (DataflowFunction) +import Lunarbox.Data.Editor.ExtendedLocation (ExtendedLocation(..)) import Lunarbox.Data.Editor.FunctionData (FunctionData) import Lunarbox.Data.Editor.FunctionName (FunctionName) import Lunarbox.Data.Editor.Location (Location) -import Lunarbox.Data.Editor.Node (Node) +import Lunarbox.Data.Editor.Node (Node, _nodeInputs) import Lunarbox.Data.Editor.Node.NodeData (NodeData, _NodeDataSelected) import Lunarbox.Data.Editor.Node.NodeId (NodeId) -import Lunarbox.Data.Editor.NodeGroup (NodeGroup) -import Lunarbox.Data.Editor.Project (Project, _ProjectFunctions, _atProjectFunction, _atProjectNode, _projectNodeGroup) +import Lunarbox.Data.Editor.NodeGroup (NodeGroup, _NodeGroupNodes) +import Lunarbox.Data.Editor.PartialConnection (PartialConnection, _from, _to) +import Lunarbox.Data.Editor.Project (Project, _ProjectFunctions, _atProjectFunction, _atProjectNode, _projectNodeGroup, compileProject) import Lunarbox.Data.Graph as G +import Lunarbox.Data.Lens (listToArrayIso) import Lunarbox.Data.Vector (Vec2) import Svg.Attributes (Color) @@ -76,6 +91,7 @@ type State , lastMousePosition :: Maybe (Vec2 Number) , nodeData :: Map (Tuple FunctionName NodeId) NodeData , functionData :: Map FunctionName FunctionData + , partialConnection :: PartialConnection } -- Lenses @@ -135,3 +151,75 @@ _panelIsOpen = prop (SProxy :: _ "panelIsOpen") _currentTab :: Lens' State Tab _currentTab = prop (SProxy :: _ "currentTab") + +_partialConnection :: Lens' State PartialConnection +_partialConnection = prop (SProxy :: _ "partialConnection") + +_partialFrom :: Lens' State ((Maybe NodeId)) +_partialFrom = _partialConnection <<< _from + +_partialTo :: Lens' State (Maybe (Tuple NodeId Int)) +_partialTo = _partialConnection <<< _to + +_currentNodeGroup :: Lens' State (Maybe NodeGroup) +_currentNodeGroup = + ( lens + ( \state -> do + currentFunction <- view _currentFunction state + preview (_nodeGroup currentFunction) state + ) + ( \state maybeValue -> + fromMaybe state do + value <- maybeValue + currentFunction <- view _currentFunction state + pure $ set (_nodeGroup currentFunction) value state + ) + ) + +_currentNodes :: Traversal' State (G.Graph NodeId Node) +_currentNodes = _currentNodeGroup <<< _Just <<< _NodeGroupNodes + +_atCurrentNode :: NodeId -> Traversal' State (Maybe Node) +_atCurrentNode id = _currentNodes <<< at id + +-- Helpers +-- Compile a project +compile :: State -> State +compile state@{ project, expression, typeMap } = + let + expression' = compileProject project + + typeMap' = + -- we only run the type inference algorithm if the expression changed + if (expression == expression') then + typeMap + else case solveExpression expression' of + Right map -> Map.delete Nowhere map + -- TODO: make it so this accounts for errors + Left _ -> mempty + in + state { expression = expression', typeMap = typeMap' } + +-- Tries connecting the pins the user selected +tryConnecting :: State -> State +tryConnecting state = + fromMaybe state do + from <- view _partialFrom state + Tuple toId toIndex <- view _partialTo state + currentNodeGroup <- view _currentNodeGroup state + let + state' = over _currentNodes (G.insertEdge from toId) state + + state'' = + set + ( _atCurrentNode toId + <<< _Just + <<< _nodeInputs + <<< listToArrayIso + <<< ix toIndex + ) + (Just from) + state' + + state''' = set _partialTo Nothing $ set _partialFrom Nothing state'' + pure $ compile state''' diff --git a/src/Data/Graph.purs b/src/Data/Graph.purs index e105b80..82dbdff 100644 --- a/src/Data/Graph.purs +++ b/src/Data/Graph.purs @@ -8,12 +8,15 @@ module Lunarbox.Data.Graph , keys , vertices , toUnfoldable + , insertEdge , topologicalSort + , edges , _Graph ) where import Prelude -import Data.Bifunctor (lmap) +import Data.Array as Array +import Data.Bifunctor (lmap, rmap) import Data.Foldable (class Foldable, foldMap, foldlDefault, foldrDefault) import Data.Graph as CG import Data.Lens (Traversal', lens, traversed, wander) @@ -99,6 +102,17 @@ vertices = map fst <<< Map.values <<< unwrap toUnfoldable :: forall u k v. Unfoldable u => Ord k => Graph k v -> u (Tuple k v) toUnfoldable (Graph m) = Map.toUnfoldable $ fst <$> m +-- Insert an edge from the start key to the end key. +insertEdge :: forall k v. Ord k => k -> k -> Graph k v -> Graph k v +insertEdge from to (Graph g) = Graph $ Map.alter (map (rmap (Set.insert to))) from g + +-- Get all the edges from a graph +edges :: forall k v u. Unfoldable u => Ord k => Graph k v -> u (Tuple k k) +edges (Graph map) = + Map.toUnfoldable map + >>= (\(Tuple from (Tuple _ to)) -> Tuple from <$> Set.toUnfoldable to) + # Array.toUnfoldable + -- no idea how to implement this so I'm using an implementation from another lib topologicalSort :: forall k v. Ord k => Graph k v -> List k topologicalSort = CG.topologicalSort <<< CG.fromMap <<< unwrap diff --git a/src/Data/Lens.purs b/src/Data/Lens.purs index 6963241..437b631 100644 --- a/src/Data/Lens.purs +++ b/src/Data/Lens.purs @@ -1,8 +1,17 @@ -module Lunarbox.Data.Lens where +module Lunarbox.Data.Lens + ( listToArrayIso + , newtypeIso + ) where -import Data.Lens (Lens', iso) +import Data.Array as Array +import Data.Lens (Lens', Iso', iso) +import Data.List as List import Data.Newtype (class Newtype, unwrap, wrap) -- Generic iso which can be used for any data type with a newtype instance newtypeIso :: forall a b. Newtype a b => Lens' a b newtypeIso = iso unwrap wrap + +-- I usually use this when I want to focus on a single element of a lsit +listToArrayIso :: forall a. Iso' (List.List a) (Array a) +listToArrayIso = iso List.toUnfoldable Array.toUnfoldable diff --git a/src/Data/String.purs b/src/Data/String.purs new file mode 100644 index 0000000..a6e2b85 --- /dev/null +++ b/src/Data/String.purs @@ -0,0 +1,11 @@ +module Lunarbox.Data.String (indent) where + +import Prelude +import Data.String.Utils (lines, unsafeRepeat) +import Data.String (joinWith) + +-- Indent a string by a number of spaces +indent :: Int -> String -> String +indent spaces = joinWith "\n" <<< map (space <> _) <<< lines + where + space = unsafeRepeat spaces " " diff --git a/src/Svg/Element.purs b/src/Svg/Element.purs new file mode 100644 index 0000000..23e8b5f --- /dev/null +++ b/src/Svg/Element.purs @@ -0,0 +1,11 @@ +module Lunarbox.Svg.Element + ( tspan + ) where + +import Halogen.HTML (ElemName(..), Node) +import Svg.Indexed as I +import Svg.Elements (element) + +-- The halogen-svg lib doesn't have this so I made my own +tspan :: forall p i. Node I.SVGtext p i +tspan = element (ElemName "tspan")