diff --git a/src/arr/compiler/compile-structs.arr b/src/arr/compiler/compile-structs.arr index 563a1e010..8d02d4e30 100644 --- a/src/arr/compiler/compile-structs.arr +++ b/src/arr/compiler/compile-structs.arr @@ -3317,14 +3317,20 @@ no-globals = globals([string-dict:], [string-dict:], [string-dict:]) reactor-optional-fields = [SD.string-dict: "last-image", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-tick", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-tick-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "to-draw", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-key", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-key-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-raw-key", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-raw-key-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "on-mouse", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-mouse-send", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-receive", {(l): A.a-name(l, A.s-type-global("Function"))}, + "on-receive-send", {(l): A.a-name(l, A.s-type-global("Function"))}, "stop-when", {(l): A.a-name(l, A.s-type-global("Function"))}, "seconds-per-tick", {(l): A.a-name(l, A.s-type-global("NumPositive"))}, + "close-when-stop", {(l): A.a-name(l, A.s-type-global("Boolean"))}, "title", {(l): A.a-name(l, A.s-type-global("String"))}, - "close-when-stop", {(l): A.a-name(l, A.s-type-global("Boolean"))} ] reactor-fields = reactor-optional-fields.set("init", {(l): A.a-any(l)}) diff --git a/src/arr/trove/reactor-events.arr b/src/arr/trove/reactor-events.arr index 49dca13c7..26a31ffa5 100644 --- a/src/arr/trove/reactor-events.arr +++ b/src/arr/trove/reactor-events.arr @@ -10,6 +10,11 @@ type Modifiers = { control :: Boolean } +# TODO: add other functions that we need +type Connector = { + handle-message-return :: (M -> Nothing) +} + data Event: | time-tick | mouse(x :: Number, y :: Number, kind :: String) @@ -17,9 +22,18 @@ data Event: | raw-key(symbol :: String, code :: Number, key :: String, action :: String, modifiers :: Modifiers) end +data ConnectedEvent: + | message(msg :: M) + | event(e :: Event) +end + data RawKeyEventType: | key-up | key-down | key-press end +data SendingHandlerResult: + | update(new-state :: S) + | send(new-state :: S, to-send :: M) +end diff --git a/src/js/trove/reactors.js b/src/js/trove/reactors.js index 263032649..72c3924af 100644 --- a/src/js/trove/reactors.js +++ b/src/js/trove/reactors.js @@ -24,7 +24,13 @@ name: "ValueSkeleton" }, "Table": { tag: "name", origin: { "import-type": "uri", uri: "builtin://global" }, - name: "Table" } + name: "Table" }, + "SendingHandlerResult": { tag: "name", + origin: { "import-type": "uri", uri: "builtin://reactor-events" }, + name: "SendingHandlerResult" }, + "ConnectedEvent": { tag: "name", + origin: { "import-type": "uri", uri: "builtin://reactor-events" }, + name: "ConnectedEvent" }, }, values: { "keypress": ["arrow", ["String"], ["local", "Event"]], @@ -35,6 +41,17 @@ "key-down": ["local", "RawKeyEventType"], "key-press": ["local", "RawKeyEventType"], + "update": ["forall", ["s", "m"], + ["arrow", ["tid", "s"], + ["tyapp", ["local", "SendingHandlerResult"], [["tid", "s"], ["tid", "m"]]]]], + "send": ["forall", ["s", "m"], + ["arrow", [["tid", "s"], ["tid", "m"]], + ["tyapp", ["local", "SendingHandlerResult"], [["tid", "s"], ["tid", "m"]]]]], + "message": ["forall", ["m"] + ["arrow", [["tid", "m"]], ["tyapp", ["local", "ConnectedEvent"], [["tid", "m"]]]]], + "event": ["forall", ["m"] + ["arrow", [["local", "Event"]], ["tyapp", ["local", "ConnectedEvent"], [["tid", "m"]]]]], + "get-value": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]], "get-instance": ["forall", ["a"], ["arrow", ["RofA"], ["tid", "a"]]], "draw": ["forall", ["a"], ["arrow", ["RofA"], "Image"]], @@ -50,7 +67,8 @@ aliases: { "Event": "ReactorEvent", "RawKeyEventType": "RawKeyEventType", - "Reactor": ["local", "Reactor"] + "Reactor": ["local", "Reactor"], + "SendingHandlerResult": "SendingHandlerResult", }, datatypes: { "Reactor": ["data", "Reactor", ["a"], [], { @@ -99,6 +117,8 @@ }; var annEvent = gtf(reactorEvents, "Event"); + var annConnectedEvent = gtf(reactorEvents, "ConnectedEvent"); + var annSendingHandlerResult = gtf(reactorEvents, "SendingHandlerResult"); var annNatural = runtime.makeFlatPredAnn(runtime.Number, runtime.makeFunction(function(val) { return jsnums.isInteger(val) && jsnums.greaterThanOrEqual(val, 0, runtime.NumberErrbacks); }, "Natural Number"), "Natural Number"); @@ -112,10 +132,50 @@ } var isEvent = gmf(reactorEvents, "is-Event"); + var isConnectedEvent = gmf(reactorEvents, "is-ConnectedEvent"); + var isSendingHandlerResult = gmf(reactorEvents, "is-SendingHandlerResult"); var externalInteractionHandler = null; var setInteract = function(newInteract) { externalInteractionHandler = newInteract; } + function callOrError(handlers, handlerName, args, cb) { + if(handlers.hasOwnProperty(handlerName + "-send")) { + var funObj = handlers[handlerName + "-send"].app; + return runtime.safeCall(function() { + return funObj.apply(funObj, args); + }, cb, "react:" + handlerName); + } + if(handlers.hasOwnProperty(handlerName)) { + var funObj = handlers[handlerName].app; + return runtime.safeCall(function() { + return funObj.apply(funObj, args); + }, cb, "react:" + handlerName); + } + else { + runtime.ffi.throwMessageException("No " + handlerName + " handler defined"); + } + } + function handleBaseEvent(handlers, init, event, cb) { + return runtime.ffi.cases(isEvent, "Event", event, { + keypress: function(key) { + return callOrError(handlers, "on-key", [init, key], cb); + }, + "time-tick": function() { + return callOrError(handlers, "on-tick", [init], cb); + }, + mouse: function(x, y, kind) { + return callOrError(handlers, "on-mouse", [init, x, y, kind], cb); + }, + "raw-key": function(key, type, caps, shift, alt, command, control) { + // NOTE(joe): we intentionally don't use all the fields above, assuming + // that users of on-raw-key are OK with consuming an event object + // rather than the fields of the event. This is mainly because typing + // out 8 parameters is pretty unreasonable, and this fancy version + // will mainly be used by folks who have gone through at least Reactive + return callOrError(handlers, "on-raw-key", [init, event], cb); + } + }); + } var makeReactor = function(init, fields) { runtime.ffi.checkArity(2, arguments, "reactor", false); c1("make-reactor", fields, annObject); @@ -201,6 +261,26 @@ return makeReactorRaw(newVal, handlers, tracing, trace.concat(thisInteractTrace)); }, "interact"); }), + "interact-connect": runtime.makeMethod1(function(self, connector) { + // connector: + // register-on-message: Message -> ... + // handle-message-return: Message -> void + // dispose: () -> void + // reconnect: () -> void + checkArity(2, arguments, "interact-connect", true); + let thisInteractTrace = []; + let tracer = null; + if (tracing) { + tracer = (newVal, oldVal, k) => { + thisInteractTrace.push(newVal); + k(); + }; + } + return runtime.safeCall( + () => externalInteractionHandler(init, handlers, tracer, connector), + (newVal) => makeReactorRaw(newVal, handlers, tracing, trace.concat(thisInteractTrace)), + "interact-connect"); + }), "start-trace": runtime.makeMethod0(function(self) { checkArity(1, arguments, "start-trace", true); return makeReactorRaw(init, handlers, true, [init]); @@ -238,24 +318,14 @@ react: runtime.makeMethod1(function(self, event) { checkArity(2, arguments, "react", true); c1("react", event, annEvent); - function callOrError(handlerName, args) { - if(handlers.hasOwnProperty(handlerName)) { - var funObj = handlers[handlerName].app; - return runtime.safeCall(function() { - return funObj.apply(funObj, args); - }, function(newVal) { - if(tracing) { - var newTrace = trace.concat([newVal]); - } - else { - var newTrace = trace; - } - return makeReactorRaw(newVal, handlers, tracing, newTrace); - }, "react:" + handlerName); + function reactCallback(newVal) { + if(tracing) { + var newTrace = trace.concat([newVal]); } else { - runtime.ffi.throwMessageException("No " + handlerName + " handler defined"); + var newTrace = trace; } + return makeReactorRaw(newVal, handlers, tracing, newTrace); } return runtime.safeCall(function() { if(handlers["stop-when"]) { @@ -267,29 +337,52 @@ }, function(stop) { if(stop) { return self; + } else { + return handleBaseEvent(handlers, init, event, reactCallback); } - else { - return runtime.ffi.cases(isEvent, "Event", event, { - keypress: function(key) { - return callOrError("on-key", [init, key]); - }, - "time-tick": function() { - return callOrError("on-tick", [init]); - }, - mouse: function(x, y, kind) { - return callOrError("on-mouse", [init, x, y, kind]); - }, - "raw-key": function(key, type, caps, shift, alt, command, control) { - // NOTE(joe): we intentionally don't use all the fields above, assuming - // that users of on-raw-key are OK with consuming an event object - // rather than the fields of the event. This is mainly because typing - // out 8 parameters is pretty unreasonable, and this fancy version - // will mainly be used by folks who have gone through at least Reactive - return callOrError("on-raw-key", [init, event]); - } + }, "react:stop-when"); + }), + respond: runtime.makeMethod2(function(self, event, connector) { + checkArity(3, arguments, "react", true); + c1("respond", event, annConnectedEvent); + function respondCallback(newVal) { + return runtime.ffi.cases(isSendingHandlerResult, "SendingHandlerResult", newVal, { + update: (val) => + runtime.makeTuple([ + makeReactorRaw(val, handlers, tracing, trace.concat([val])), + runtime.ffi.makeNone(), + ]), + send: (val, msg) => + runtime.safeCall( + () => gf(connector, "handle-message-return").app(msg), + () => + runtime.makeTuple([ + makeReactorRaw(val, handlers, tracing, trace.concat([val])), + runtime.ffi.makeSome(msg), + ]), + ), + }); + } + return runtime.safeCall( + () => { + if (handlers["stop-when"]) { + return handlers["stop-when"].app(init); + } else { + return false; + } + }, + (stop) => { + if (stop) { + return self; + } else { + return runtime.ffi.cases(isConnectedEvent, "ConnectedEvent", event, { + message: (msg) => + callOrError(handlers, "on-receive", [init, msg], respondCallback), + event: (e) => handleBaseEvent(handlers, init, e, respondCallback), }); } - }, "react:stop-when"); + } + ) }), "is-stopped": runtime.makeMethod0(function(self) { checkArity(1, arguments, "is-stopped", true); @@ -381,6 +474,10 @@ "key-down": gmf(reactorEvents, "key-down"), "key-press": gmf(reactorEvents, "key-press"), "make-reactor": F(makeReactor, "make-reactor"), + "update": gmf(reactorEvents, "update"), + "send": gmf(reactorEvents, "send"), + "message": gmf(reactorEvents, "message"), + "event": gmf(reactorEvents, "event"), "get-value": F(getValue, "get-value"), "get-instance": F(getValue, "get-instance"),