@@ -0,0 +1,604 @@
1+(function() {
2+'use strict';
3+
4+if (!window.location.origin) {
5+ window.location.origin =
6+ window.location.protocol + "//" +
7+ window.location.hostname +
8+ (window.location.port ? (':' + window.location.port) : '');
9+}
10+
11+Elm.fullscreenDebugHooks = function(module, hotSwapState /* =undefined */) {
12+ var exposedDebugger = {};
13+ function debuggerAttach(module, hotSwapState) {
14+ return {
15+ make: function(runtime) {
16+ var wrappedModule = debugModule(module, runtime);
17+ exposedDebugger = debuggerInit(wrappedModule, runtime, hotSwapState);
18+ return wrappedModule.debuggedModule;
19+ }
20+ }
21+ }
22+ var mainHandle = Elm.fullscreen(debuggerAttach(module,hotSwapState));
23+ mainHandle.debugger = exposedDebugger;
24+ return mainHandle;
25+};
26+
27+var EVENTS_PER_SAVE = 100;
28+
29+function debugModule(module, runtime) {
30+ var programPaused = false;
31+ var recordedEvents = [];
32+ var asyncCallbacks = [];
33+ var snapshots = [];
34+ var watchTracker = Elm.Native.Debug.make(runtime).watchTracker;
35+ var pauseTime = 0;
36+ var eventsUntilSnapshot = EVENTS_PER_SAVE;
37+ runtime.debuggerStatus = runtime.debuggerStatus || {};
38+ runtime.debuggerStatus.eventCounter = runtime.debuggerStatus.eventCounter || 0;
39+
40+ // runtime is the prototype of wrappedRuntime
41+ // so we can access all runtime properties too
42+ var wrappedRuntime = Object.create(runtime);
43+ wrappedRuntime.notify = notifyWrapper;
44+ wrappedRuntime.setTimeout = setTimeoutWrapper;
45+
46+ // make a copy of the wrappedRuntime
47+ var assignedPropTracker = Object.create(wrappedRuntime);
48+ var debuggedModule = module.make(assignedPropTracker);
49+
50+ // make sure the signal graph is actually a signal & extract the visual model
51+ if ( !('recv' in debuggedModule.main) ) {
52+ debuggedModule.main = Elm.Signal.make(runtime).constant(debuggedModule.main);
53+ }
54+
55+ // The main module stores imported modules onto the runtime.
56+ // To ensure only one instance of each module is created,
57+ // we assign them back on the original runtime object.
58+ Object.keys(assignedPropTracker).forEach(function(key) {
59+ runtime[key] = assignedPropTracker[key];
60+ });
61+
62+ var signalGraphNodes = flattenSignalGraph(wrappedRuntime.inputs);
63+ var tracePath = tracePathInit(runtime, debuggedModule.main);
64+
65+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
66+
67+ function notifyWrapper(id, v) {
68+ var timestep = runtime.timer.now();
69+
70+ if (programPaused) {
71+ // ignore async events generated while playing back
72+ // or user events while program is paused
73+ return false;
74+ }
75+ else {
76+ recordEvent(id, v, timestep);
77+ var changed = runtime.notify(id, v, timestep);
78+ snapshotOnCheckpoint();
79+ if (parent.window) {
80+ parent.window.postMessage("elmNotify", window.location.origin);
81+ }
82+ return changed;
83+ }
84+ };
85+
86+ function setTimeoutWrapper(func, delayMs) {
87+ if (programPaused) {
88+ // Don't push timers and such to the callback stack while we're paused.
89+ // It causes too many callbacks to be fired during unpausing.
90+ return 0;
91+ }
92+ var cbObj = { func:func, delayMs:delayMs, timerId:0, executed:false };
93+ var timerId = setTimeout(function() {
94+ cbObj.executed = true;
95+ func();
96+ }, delayMs);
97+ cbObj.timerId = timerId;
98+ asyncCallbacks.push(cbObj);
99+ return timerId;
100+ }
101+
102+ function recordEvent(id, v, timestep) {
103+ watchTracker.pushFrame();
104+ recordedEvents.push({ id:id, value:v, timestep:timestep });
105+ runtime.debuggerStatus.eventCounter += 1;
106+ }
107+
108+ function clearAsyncCallbacks() {
109+ asyncCallbacks.forEach(function(timer) {
110+ if (!timer.executed) {
111+ clearTimeout(timer.timerId);
112+ }
113+ });
114+ }
115+
116+ function clearRecordedEvents() {
117+ recordedEvents = [];
118+ runtime.debuggerStatus.eventCounter = 0;
119+ }
120+
121+ function getRecordedEventsLength() {
122+ return recordedEvents.length;
123+ }
124+
125+ function getRecordedEventAt(i) {
126+ return recordedEvents[i];
127+ }
128+
129+ function copyRecordedEvents() {
130+ return recordedEvents.slice();
131+ }
132+
133+ function loadRecordedEvents(events) {
134+ recordedEvents = events.slice();
135+ }
136+
137+ function clearSnapshots() {
138+ snapshots = [snapshotSignalGraph(signalGraphNodes)];
139+ }
140+
141+ function getSnapshotAt(i) {
142+ var snapshotEvent = Math.floor(i / EVENTS_PER_SAVE);
143+ assert(snapshotEvent < snapshots.length && snapshotEvent >= 0,
144+ "Out of bounds index: " + snapshotEvent);
145+ return snapshots[snapshotEvent];
146+ }
147+
148+ function snapshotOnCheckpoint() {
149+ if (eventsUntilSnapshot === 1) {
150+ snapshots.push(snapshotSignalGraph(signalGraphNodes));
151+ eventsUntilSnapshot = EVENTS_PER_SAVE;
152+ } else {
153+ eventsUntilSnapshot -= 1;
154+ }
155+ }
156+
157+ function setPaused() {
158+ programPaused = true;
159+ clearAsyncCallbacks();
160+ pauseTime = Date.now();
161+ tracePath.stopRecording();
162+ preventInputEvents();
163+ }
164+
165+ function setContinue(position) {
166+ var pauseDelay = Date.now() - pauseTime;
167+ runtime.timer.addDelay(pauseDelay);
168+ programPaused = false;
169+
170+ // we need to dump the events that are ahead of where we're continuing.
171+ var lastSnapshotPosition = Math.floor(position / EVENTS_PER_SAVE);
172+ eventsUntilSnapshot = EVENTS_PER_SAVE - (position % EVENTS_PER_SAVE);
173+ snapshots = snapshots.slice(0, lastSnapshotPosition + 1);
174+
175+ if (position < getRecordedEventsLength()) {
176+ var lastEventTime = recordedEvents[position].timestep;
177+ var scrubTime = runtime.timer.now() - lastEventTime;
178+ runtime.timer.addDelay(scrubTime);
179+ }
180+
181+ recordedEvents = recordedEvents.slice(0, position);
182+ tracePath.clearTracesAfter(position);
183+ runtime.debuggerStatus.eventCounter = position;
184+ executeCallbacks(asyncCallbacks);
185+ permitInputEvents();
186+
187+ tracePath.startRecording();
188+ }
189+
190+ function getPaused() {
191+ return programPaused;
192+ }
193+
194+ function preventInputEvents(){
195+ var events =
196+ [ "click", "mousemove", "mouseup", "mousedown", "mouseclick"
197+ , "keydown", "keypress", "keyup", "touchstart", "touchend"
198+ , "touchcancel", "touchleave", "touchmove", "pointermove"
199+ , "pointerdown", "pointerup", "pointerover", "pointerout"
200+ , "pointerenter", "pointerleave", "pointercancel"
201+ ];
202+
203+ var ignore = function(e) {
204+ var evt = e ? e : window.event;
205+ if (evt.stopPropagation) {
206+ evt.stopPropagation();
207+ }
208+ if (evt.cancelBubble !== null) {
209+ evt.cancelBubble = true;
210+ }
211+ if (evt.preventDefault) {
212+ evt.preventDefault();
213+ }
214+ return false;
215+ };
216+
217+ var ignoringDiv = document.getElementById("elmEventIgnorer");
218+ if (!ignoringDiv) {
219+ ignoringDiv = document.createElement("div");
220+ ignoringDiv.id = "elmEventIgnorer";
221+ ignoringDiv.style.position = "absolute";
222+ ignoringDiv.style.top = "0px";
223+ ignoringDiv.style.left = "0px";
224+ ignoringDiv.style.width = "100%";
225+ ignoringDiv.style.height = "100%";
226+
227+ for (var i = events.length; i-- ;) {
228+ ignoringDiv.addEventListener(events[i], ignore, true);
229+ }
230+ runtime.node.appendChild(ignoringDiv);
231+ }
232+ }
233+
234+ function permitInputEvents(){
235+ var ignoringDiv = document.getElementById("elmEventIgnorer");
236+ ignoringDiv.parentNode.removeChild(ignoringDiv);
237+ }
238+
239+ return {
240+ debuggedModule: debuggedModule,
241+ signalGraphNodes: signalGraphNodes,
242+ initialSnapshot: snapshotSignalGraph(signalGraphNodes),
243+ initialAsyncCallbacks: asyncCallbacks.slice(),
244+ // API functions
245+ clearAsyncCallbacks: clearAsyncCallbacks,
246+ clearRecordedEvents: clearRecordedEvents,
247+ getRecordedEventsLength: getRecordedEventsLength,
248+ getRecordedEventAt: getRecordedEventAt,
249+ copyRecordedEvents: copyRecordedEvents,
250+ loadRecordedEvents: loadRecordedEvents,
251+ clearSnapshots: clearSnapshots,
252+ getSnapshotAt: getSnapshotAt,
253+ snapshotOnCheckpoint: snapshotOnCheckpoint,
254+ getPaused: getPaused,
255+ setPaused: setPaused,
256+ setContinue: setContinue,
257+ tracePath: tracePath,
258+ watchTracker: watchTracker
259+ };
260+}
261+
262+// The debuggerHistory variable is passed in on hotswap. It represents
263+// the a state of the debugger for it to assume during init. It contains
264+// the paused state of the debugger, the recorded events, and the current
265+// event being processed.
266+function debuggerInit(debugModule, runtime, debuggerHistory /* =undefined */) {
267+ var currentEventIndex = 0;
268+
269+ function resetProgram(position) {
270+ var closestSnapshot = debugModule.getSnapshotAt(position);
271+ debugModule.clearAsyncCallbacks();
272+ restoreSnapshot(debugModule.signalGraphNodes, closestSnapshot);
273+ redrawGraphics();
274+ }
275+
276+ function restartProgram() {
277+ pauseProgram();
278+ resetProgram(0);
279+ debugModule.watchTracker.clear();
280+ debugModule.tracePath.clearTraces();
281+ debugModule.clearRecordedEvents();
282+ debugModule.clearSnapshots();
283+ debugModule.setContinue(0);
284+ executeCallbacks(debugModule.initialAsyncCallbacks);
285+ }
286+
287+ function pauseProgram() {
288+ debugModule.setPaused();
289+ currentEventIndex = debugModule.getRecordedEventsLength();
290+ }
291+
292+ function continueProgram() {
293+ if (debugModule.getPaused())
294+ {
295+ var closestSnapshotIndex =
296+ Math.floor(currentEventIndex / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
297+ resetProgram(currentEventIndex);
298+ var continueIndex = currentEventIndex;
299+ currentEventIndex = closestSnapshotIndex;
300+ stepTo(continueIndex);
301+ debugModule.setContinue(currentEventIndex);
302+ }
303+ }
304+
305+ function stepTo(index) {
306+ if (!debugModule.getPaused()) {
307+ debugModule.setPaused();
308+ resetProgram();
309+ }
310+
311+ if (index < 0 || index > getMaxSteps()) {
312+ throw "Index out of bounds: " + index;
313+ }
314+
315+ if (index < currentEventIndex) {
316+ var closestSnapshotIndex = Math.floor(index / EVENTS_PER_SAVE) * EVENTS_PER_SAVE;
317+ resetProgram(index);
318+ currentEventIndex = closestSnapshotIndex;
319+ }
320+
321+ while (currentEventIndex < index) {
322+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
323+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
324+
325+ currentEventIndex += 1;
326+ }
327+ }
328+
329+ function getMaxSteps() {
330+ return debugModule.getRecordedEventsLength();
331+ }
332+
333+ function redrawGraphics() {
334+ var main = debugModule.debuggedModule.main
335+ for (var i = main.kids.length ; i-- ; ) {
336+ main.kids[i].recv(runtime.timer.now(), true, main.id);
337+ }
338+ }
339+
340+ function getHotSwapState() {
341+ var continueIndex = currentEventIndex;
342+ if (!debugModule.getPaused()) {
343+ continueIndex = getMaxSteps();
344+ }
345+ return {
346+ paused: debugModule.getPaused(),
347+ recordedEvents: debugModule.copyRecordedEvents(),
348+ currentEventIndex: continueIndex
349+ };
350+ }
351+
352+ function dispose() {
353+ var parentNode = runtime.node.parentNode;
354+ parentNode.removeChild(debugModule.tracePath.canvas);
355+ parentNode.removeChild(runtime.node);
356+ }
357+
358+ if (debuggerHistory) {
359+ // The problem is that we want to previous paused state. But
360+ // by the time JS reaches here, the old code has been swapped out
361+ // and the new modules are being generated. So we can ask the
362+ // debugging console what it thinks the pause state is and go
363+ // from there.
364+ var paused = debuggerHistory.paused;
365+ debugModule.setPaused();
366+ debugModule.loadRecordedEvents(debuggerHistory.recordedEvents);
367+ var index = getMaxSteps();
368+ runtime.debuggerStatus.eventCounter = 0;
369+ debugModule.tracePath.clearTraces();
370+
371+ // draw new trace path
372+ debugModule.tracePath.startRecording();
373+ while(currentEventIndex < index) {
374+ var nextEvent = debugModule.getRecordedEventAt(currentEventIndex);
375+ runtime.debuggerStatus.eventCounter += 1;
376+ runtime.notify(nextEvent.id, nextEvent.value, nextEvent.timestep);
377+ debugModule.snapshotOnCheckpoint();
378+ currentEventIndex += 1;
379+ }
380+ debugModule.tracePath.stopRecording();
381+
382+ stepTo(debuggerHistory.currentEventIndex);
383+ if (!paused) {
384+ debugModule.setContinue(debuggerHistory.currentEventIndex);
385+ }
386+ }
387+
388+ runtime.node.parentNode.appendChild(debugModule.tracePath.canvas);
389+
390+ var elmDebugger = {
391+ restart: restartProgram,
392+ pause: pauseProgram,
393+ kontinue: continueProgram,
394+ getMaxSteps: getMaxSteps,
395+ stepTo: stepTo,
396+ getPaused: debugModule.getPaused,
397+ getHotSwapState: getHotSwapState,
398+ dispose: dispose,
399+ allNodes: debugModule.signalGraphNodes,
400+ watchTracker: debugModule.watchTracker
401+ };
402+
403+ return elmDebugger;
404+}
405+
406+function Point(x, y) {
407+ this.x = x;
408+ this.y = y;
409+
410+ this.translate = function(x, y) {
411+ return new Point(this.x + x, this.y + y);
412+ }
413+
414+ this.equals = function(p) {
415+ return this.x == p.x && this.y == p.y;
416+ }
417+}
418+
419+function tracePathInit(runtime, signalGraphMain) {
420+ var List = Elm.List.make(runtime);
421+ var Signal = Elm.Signal.make(runtime);
422+ var tracePathNode = A2(Signal.lift, graphicsUpdate, signalGraphMain);
423+ var tracePathCanvas = createCanvas();
424+ var tracePositions = {};
425+ var recordingTraces = true;
426+
427+ function findPositions(currentScene) {
428+ var positions = {};
429+ function processElement(elem, offset) {
430+ if (elem.element.ctor == "Custom" && elem.element.type == "Collage")
431+ {
432+ List.map(F2(processForm)(offset))(elem.element.model.forms);
433+ }
434+ }
435+
436+ function processForm(offset, form) {
437+ if (form.form.ctor == "FElement")
438+ {
439+ processElement(form.form._0, offset.translate(form.x, -form.y));
440+ }
441+ if (form.form.ctor == "FGroup")
442+ {
443+ var newOffset = offset.translate(form.x, -form.y);
444+ List.map(F2(processForm)(newOffset))(form.form._1);
445+ }
446+ if (form.debugTracePathId)
447+ {
448+ positions[form.debugTracePathId] = new Point(form.x + offset.x, -form.y + offset.y);
449+ }
450+ }
451+
452+ processElement(currentScene, new Point(0, 0));
453+ return positions;
454+ }
455+
456+ function appendPositions(positions) {
457+ for (var id in positions) {
458+ var pos = positions[id];
459+ if (tracePositions.hasOwnProperty(id)) {
460+ tracePositions[id].push(pos);
461+ }
462+ else {
463+ tracePositions[id] = [pos];
464+ }
465+ if (tracePositions[id].length < runtime.debuggerStatus.eventCounter) {
466+ var padCount = runtime.debuggerStatus.eventCounter - tracePositions[id].length;
467+ var lastTracePosition = tracePositions[id][tracePositions[id].length - 1];
468+ for (var i = padCount; i--;) {
469+ tracePositions[id].push(lastTracePosition)
470+ }
471+ }
472+ assert(tracePositions[id].length === runtime.debuggerStatus.eventCounter,
473+ "We don't have a 1-1 mapping of trace positions to events");
474+ }
475+ }
476+
477+ function graphicsUpdate(currentScene) {
478+ if (!recordingTraces) {
479+ return;
480+ }
481+
482+ var ctx = tracePathCanvas.getContext('2d');
483+ ctx.clearRect(0, 0, tracePathCanvas.width, tracePathCanvas.height);
484+
485+ ctx.save();
486+ ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
487+ appendPositions(findPositions(currentScene));
488+ for (var id in tracePositions)
489+ {
490+ ctx.beginPath();
491+ var points = tracePositions[id];
492+ for (var i=0; i < points.length; i++)
493+ {
494+ var p = points[i];
495+ if (i == 0) {
496+ ctx.moveTo(p.x, p.y);
497+ }
498+ else {
499+ ctx.lineTo(p.x, p.y);
500+ }
501+ }
502+ ctx.lineWidth = 1;
503+ ctx.strokeStyle = "rgba(50, 50, 50, 0.4)";
504+ ctx.stroke();
505+ }
506+
507+ ctx.restore();
508+ }
509+
510+ function clearTraces() {
511+ tracePositions = {};
512+ }
513+
514+ function stopRecording() {
515+ recordingTraces = false;
516+ }
517+
518+ function startRecording() {
519+ recordingTraces = true;
520+ }
521+
522+ function clearTracesAfter(position) {
523+ var newTraces = {};
524+ for (var id in tracePositions) {
525+ newTraces[id] = tracePositions[id].slice(0,position);
526+ }
527+ tracePositions = newTraces;
528+ }
529+
530+ return {
531+ graphicsUpdate: graphicsUpdate,
532+ canvas: tracePathCanvas,
533+ clearTraces: clearTraces,
534+ clearTracesAfter: clearTracesAfter,
535+ stopRecording: stopRecording,
536+ startRecording: startRecording
537+ }
538+}
539+
540+function executeCallbacks(callbacks) {
541+ callbacks.forEach(function(timer) {
542+ if (!timer.executed) {
543+ var func = timer.func;
544+ timer.executed = true;
545+ func();
546+ }
547+ });
548+}
549+
550+function createCanvas() {
551+ var c = document.createElement('canvas');
552+ c.width = window.innerWidth;
553+ c.height = window.innerHeight;
554+ c.style.position = "absolute";
555+ c.style.top = "0";
556+ c.style.left = "0";
557+ c.style.pointerEvents = "none";
558+ return c;
559+}
560+
561+function assert(bool, msg) {
562+ if (!bool) {
563+ throw "Assertion error: " + msg;
564+ }
565+}
566+
567+function snapshotSignalGraph(signalGraphNodes) {
568+ var nodeValues = [];
569+
570+ signalGraphNodes.forEach(function(node) {
571+ nodeValues.push({ value: node.value, id: node.id });
572+ });
573+
574+ return nodeValues;
575+};
576+
577+function restoreSnapshot(signalGraphNodes, snapshot) {
578+ assert(signalGraphNodes.length == snapshot.length,
579+ "saved program state has wrong length");
580+ for (var i=0; i < signalGraphNodes.length; i++) {
581+ var node = signalGraphNodes[i];
582+ var state = snapshot[i];
583+ assert(node.id == state.id, "the nodes moved position");
584+
585+ node.value = state.value;
586+ }
587+}
588+
589+function flattenSignalGraph(nodes) {
590+ var nodesById = {};
591+
592+ function addAllToDict(node) {
593+ nodesById[node.id] = node;
594+ node.kids.forEach(addAllToDict);
595+ }
596+ nodes.forEach(addAllToDict);
597+
598+ var allNodes = Object.keys(nodesById).sort().map(function(key) {
599+ return nodesById[key];
600+ });
601+ return allNodes;
602+};
603+
604+}());