222 lines
8.1 KiB
TypeScript
222 lines
8.1 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
import { Dataspace, Cap, Value, IdentitySet, Ref, Bytes, Observe, Turn, Reader, genericEmbeddedTypeDecode, Schemas, GenericEmbedded, Dictionary, fromJS } from "@syndicate-lang/core";
|
|
import { QuasiValue as Q } from "@syndicate-lang/core";
|
|
const Trace = Schemas.trace;
|
|
import * as P from './pict';
|
|
|
|
import { boot as bootHtml, Anchor, makeTemplate, UIEvent, UIFragment } from "@syndicate-lang/html";
|
|
// const svg = makeTemplate('svg');
|
|
|
|
assertion type URLContent(url: string, content: Bytes | false);
|
|
|
|
const SWIMLANE_WIDTH = 100;
|
|
const LIFELINE_WIDTH = 10;
|
|
|
|
export function main() {
|
|
Dataspace.boot(ds => {
|
|
P.init();
|
|
bootHtml(ds);
|
|
bootFetcher(ds);
|
|
bootRenderer(ds);
|
|
});
|
|
}
|
|
|
|
function bootFetcher(ds: Ref) {
|
|
spawn named 'fetcher' {
|
|
at ds {
|
|
during Observe({
|
|
"pattern": :pattern URLContent(\Q.lit($url: string), \_)
|
|
}) => spawn named ['fetch', url] {
|
|
const facet = Turn.activeFacet;
|
|
facet.preventInertCheck();
|
|
fetch(url)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
console.log('unsuccessful request', url, response);
|
|
facet.turn(() => {
|
|
assert URLContent(url, false);
|
|
});
|
|
} else {
|
|
response.arrayBuffer().then(bs => facet.turn(() => {
|
|
assert URLContent(url, Bytes.from(bs));
|
|
}));
|
|
}
|
|
})
|
|
.catch(error => facet.turn(() => {
|
|
console.error('failed request', url, error);
|
|
assert URLContent(url, false);
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function bootRenderer(ds: Ref) {
|
|
spawn named 'renderer' {
|
|
const r = new Renderer(ds);
|
|
at ds {
|
|
// const ui = new Anchor();
|
|
// field y: number = 50;
|
|
// assert ui.html('#playground', svg`<text y="${y.value}">foo</text>`);
|
|
// on message UIEvent(ui.fragmentId, '.', 'click', $_e) => {
|
|
// y.value += 20;
|
|
// }
|
|
// // Turn.active.every(1000 / 30.0, () => y.value += 5);
|
|
|
|
const traceUrl = 'd2';
|
|
during URLContent(traceUrl, false) => {
|
|
alert("Couldn't fetch trace file: " + JSON.stringify(traceUrl));
|
|
}
|
|
during URLContent(traceUrl, $bs: Bytes) => {
|
|
const vs = new Reader(bs.fromUtf8(), {
|
|
embeddedDecode: genericEmbeddedTypeDecode,
|
|
}).readToEnd();
|
|
for (const v of vs) {
|
|
r.addTraceEntry(Trace.asTraceEntry(v));
|
|
}
|
|
r.container.scrollLeft =
|
|
(r.playground.width.baseVal.value - r.container.getBoundingClientRect().width)
|
|
/ 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Swimlane {
|
|
readonly actorId: Schemas.trace.ActorId<GenericEmbedded>;
|
|
readonly renderer: Renderer;
|
|
readonly laneNumber: number;
|
|
readonly startY: number;
|
|
readonly entries: Array<Schemas.trace.TraceEntry<GenericEmbedded>> = [];
|
|
readonly lifeline = P.rect([LIFELINE_WIDTH, 0], {"class": "lifeline"});
|
|
|
|
// readonly lifeline = svg.tag(SVGRectElement)`<rect class="lifeline"/>`;
|
|
|
|
constructor (actorId: Schemas.trace.ActorId<GenericEmbedded>, laneNumber: number, renderer: Renderer) {
|
|
this.actorId = actorId;
|
|
this.renderer = renderer;
|
|
this.laneNumber = laneNumber;
|
|
this.startY = renderer.ypos;
|
|
|
|
new P.Point(this.midX - LIFELINE_WIDTH / 2, this.startY).apply(this.lifeline);
|
|
this.renderer.ypos += 10;
|
|
this.lifeline.setAttribute('height', '' + (this.renderer.ypos - this.startY));
|
|
this.renderer.underlay.appendChild(this.lifeline);
|
|
}
|
|
|
|
get midX(): number {
|
|
return this.laneNumber * SWIMLANE_WIDTH;
|
|
}
|
|
|
|
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
|
|
this.entries.push(entry);
|
|
|
|
// const n = this.renderer.placard(
|
|
// 'test1', this.renderer.textNode('label', fromJS(entry.item).asPreservesText()));
|
|
// const b = this.renderer.measureBBox(n);
|
|
// n.setAttribute('x', '' + (this.laneNumber * SWIMLANE_WIDTH - b.width / 2));
|
|
// n.setAttribute('y', '' + this.renderer.ypos);
|
|
|
|
const n = P.placard(P.text(fromJS(entry.item).asPreservesText(), {"class": "label"}),
|
|
{"class": "test1"});
|
|
const b = P.bbox(n);
|
|
b.withTopMid([this.midX, this.renderer.ypos]).apply(n);
|
|
|
|
this.renderer.ypos += b.height + 10;
|
|
this.renderer.overlay.appendChild(n);
|
|
this.lifeline.setAttribute('height', '' + (this.renderer.ypos - this.startY));
|
|
if (entry.item._variant === "stop") {
|
|
this.renderer.releaseSwimlane(this.laneNumber);
|
|
}
|
|
this.renderer.adjustViewport();
|
|
}
|
|
}
|
|
|
|
class Renderer {
|
|
ds: Ref;
|
|
readonly container: HTMLDivElement = document.getElementById('container')! as any;
|
|
readonly playground: SVGSVGElement = document.getElementById('playground')! as any;
|
|
readonly underlay: SVGGElement = this.playground.getElementById('underlay')! as any;
|
|
readonly overlay: SVGGElement = this.playground.getElementById('overlay')! as any;
|
|
readonly swimlanes: Dictionary<GenericEmbedded, Swimlane> = new Dictionary();
|
|
readonly busyLanes = new IdentitySet<number>();
|
|
ypos = 0;
|
|
|
|
constructor (ds: Ref) {
|
|
this.ds = ds;
|
|
|
|
this.adjustViewport();
|
|
at this.ds {
|
|
on asserted UIFragment($_fragmentId, $_newSelector, $_newHtml, $_newOrderBy) => {
|
|
this.adjustViewport();
|
|
}
|
|
}
|
|
}
|
|
|
|
// temporarilyInDom<R>(e: SVGElement, f: () => R): R {
|
|
// this.playground.appendChild(e);
|
|
// const result = f();
|
|
// this.playground.removeChild(e);
|
|
// return result;
|
|
// }
|
|
|
|
// measureClientRect(e: SVGElement) {
|
|
// return this.temporarilyInDom(e, () => e.getBoundingClientRect());
|
|
// }
|
|
|
|
// measureBBox(e: SVGGraphicsElement) {
|
|
// return this.temporarilyInDom(e, () => e.getBBox());
|
|
// }
|
|
|
|
// measureTextLength(e: SVGTextContentElement) {
|
|
// return this.temporarilyInDom(e, () => e.getComputedTextLength());
|
|
// }
|
|
|
|
// textNode(klass: string, text: string): SVGTextElement {
|
|
// return svg.tag(SVGTextElement)`<text class="${klass}">${text}</text>`;
|
|
// }
|
|
|
|
// placard(klass: string, n: SVGGraphicsElement): SVGSVGElement {
|
|
// const b = this.measureBBox(n);
|
|
// const [px, py] = [12, 6];
|
|
// const base = svg.tag()`<rect class="${klass}" width="${b.width + px * 2}" height="${b.height + py * 2}"/>`;
|
|
// return svg.tag(SVGSVGElement)`<svg class="placard">${base}<g transform="translate(${px} ${py})">${n}</g></svg>`;
|
|
// }
|
|
|
|
adjustViewport() {
|
|
const bbox = this.playground.getBBox();
|
|
this.playground.viewBox.baseVal.x = bbox.x - 10;
|
|
this.playground.viewBox.baseVal.y = bbox.y - 10;
|
|
this.playground.viewBox.baseVal.width = bbox.width + 20;
|
|
this.playground.viewBox.baseVal.height = bbox.height + 20;
|
|
this.playground.width.baseVal.value = bbox.width + 20;
|
|
this.playground.height.baseVal.value = bbox.height + 20;
|
|
}
|
|
|
|
allocateSwimlane(): number {
|
|
let i = 0;
|
|
while (this.busyLanes.has(i)) i++;
|
|
this.busyLanes.add(i);
|
|
return i;
|
|
}
|
|
|
|
releaseSwimlane(n: number) {
|
|
this.busyLanes.delete(n);
|
|
}
|
|
|
|
swimlaneFor(actorId: Schemas.trace.ActorId<GenericEmbedded>): Swimlane {
|
|
let k = fromJS(actorId);
|
|
let s = this.swimlanes.get(k);
|
|
if (s === void 0) {
|
|
this.swimlanes.set(k, s = new Swimlane(actorId, this.allocateSwimlane(), this));
|
|
}
|
|
return s;
|
|
}
|
|
|
|
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
|
|
this.swimlaneFor(entry.actor).addTraceEntry(entry);
|
|
}
|
|
}
|