syndicate-render-msd/src/index.ts

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);
}
}