diff --git a/css/ui.css b/css/ui.css index 24804169..ce0ba4bd 100644 --- a/css/ui.css +++ b/css/ui.css @@ -713,7 +713,7 @@ div.asset_toolbar { } .tree-header { display: flex; - border: 2px solid #555; + border: 1px solid #555; border-radius:8px; color: #fff; background-color:#666; @@ -777,16 +777,18 @@ div.asset_toolbar { .scripting-cell canvas { min-height: 20em; max-width: 95%; - border: 2px solid #222; + border: 0; outline-color: #ccc; background: #000; - padding: 4px; - margin: 1px; + padding: 0; + margin: 0; image-rendering: pixelated; image-rendering: crisp-edges; } -.scripting-cell canvas:hover { - border-color:#aaa; +.scripting-cell pre { + background-color: #333; + border: 1px inset #666; + color: #99dd99; } .scripting-flex canvas { min-height: 2vw; @@ -803,6 +805,7 @@ div.scripting-color { padding: 0.5em; min-width: 3em; min-height: 3em; + border: 3px solid black; } div.scripting-color span { visibility: hidden; @@ -818,12 +821,13 @@ div.scripting-grid { div.scripting-flex { display: flex; flex-wrap: wrap; + align-items: center; } div.scripting-select > div { border-radius: 2px; border-style: dotted; border-color: transparent; - + margin: 0.25em; } div.scripting-select > div:hover { border-color: #eee; diff --git a/src/common/script/env.ts b/src/common/script/env.ts index f518f34b..c339cebd 100644 --- a/src/common/script/env.ts +++ b/src/common/script/env.ts @@ -8,6 +8,8 @@ import * as output from "./lib/output"; import * as color from "./lib/color"; import * as scriptui from "./lib/scriptui"; +export const PROP_CONSTRUCTOR_NAME = "$$consname"; + export interface Cell { id: string; object?: any; @@ -175,6 +177,7 @@ export class Environment { if (o == null) return; if (checked.has(o)) return; if (typeof o === 'object') { + o[PROP_CONSTRUCTOR_NAME] = Object.getPrototypeOf(o).constructor.name; if (o.length > 100) return; // big array, don't bother if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother checked.add(o); // so we don't recurse if cycle @@ -261,8 +264,10 @@ export class Environment { } getLoadableState() { let updated = null; - for (let [key, value] of Object.entries(this.declvars)) { - // TODO: use Loadable + // TODO: use Loadable + // TODO: visit children? + // TODO: doesn't work + for (let [key, value] of Object.entries(this.obj)) { if (typeof value['$$getstate'] === 'function') { let loadable = value as io.Loadable; if (updated == null) updated = {}; diff --git a/src/common/script/lib/bitmap.ts b/src/common/script/lib/bitmap.ts index 797fef60..15f1c2b7 100644 --- a/src/common/script/lib/bitmap.ts +++ b/src/common/script/lib/bitmap.ts @@ -10,6 +10,8 @@ export type PixelMapFunction = (x: number, y: number) => number; export abstract class AbstractBitmap { aspect? : number; // aspect ratio, null == default == 1:1 + style? : {} = {}; // CSS styles (TODO: other elements?) + constructor( public readonly width: number, public readonly height: number, diff --git a/src/common/script/lib/io.ts b/src/common/script/lib/io.ts index 710f5184..11e846be 100644 --- a/src/common/script/lib/io.ts +++ b/src/common/script/lib/io.ts @@ -9,8 +9,6 @@ var $$store: WorkingStore; var $$data: {} = {}; // events var $$seq = 0; -// if an event is specified, it goes here -export const EVENT_KEY = "$$event"; export function $$setupFS(store: WorkingStore) { $$store = store; @@ -49,6 +47,14 @@ export namespace data { } return object; } + export function get(key: string) { + return $$data && $$data[key]; + } + export function set(key: string, value: object) { + if ($$data) { + $$data[key] = value; + } + } } export class IOWaitError extends Error { @@ -128,67 +134,10 @@ export function splitlines(text: string) : string[] { } -// an object that can become interactive, identified by ID -export interface Interactive { - $$interact: InteractionRecord; -} - -export interface InteractEvent { - interactid : number; - type: string; - x?: number; - y?: number; - button?: boolean; -} - -// InteractionRecord maps a target object to an interaction ID -// the $$callback is used once per script eval, then gets nulled -// whether or not it's invoked -// event comes from $$data.$$event -export class InteractionRecord implements Loadable { - interactid : number; - lastevent : {} = null; - constructor( - public readonly interacttarget: Interactive, - private $$callback - ) { - } - $$setstate(newstate: {interactid: number}) { - this.interactid = newstate.interactid; - this.interacttarget.$$interact = this; - let event : InteractEvent = $$data[EVENT_KEY]; - if (event && event.interactid == this.interactid) { - if (this.$$callback) { - this.$$callback(event); - } - this.lastevent = event; - $$data[EVENT_KEY] = null; - } - this.$$callback = null; - } - $$getstate() { - //TODO: this isn't always cleared before we serialize (e.g. if exception or move element) - this.$$callback = null; - return this; - } -} - -export function isInteractive(obj: object): obj is Interactive { - return !!((obj as Interactive).$$interact); -} - -export function interact(object: any, callback) : InteractionRecord { - // TODO: limit to Bitmap, etc - if (typeof object === 'object') { - return new InteractionRecord(object, callback); - } - throw new Error(`This object is not capable of interaction.`); -} - // TODO: what if this isn't top level? export class Mutable implements Loadable { value : T; - constructor(public readonly initial : T) { + constructor(initial : T) { this.value = initial; } $$setstate(newstate) { @@ -199,3 +148,6 @@ export class Mutable implements Loadable { } } +export function mutable(obj: object) : object { + return new Mutable(obj); +} diff --git a/src/common/script/lib/scriptui.ts b/src/common/script/lib/scriptui.ts index 3c8a38c7..08d990b1 100644 --- a/src/common/script/lib/scriptui.ts +++ b/src/common/script/lib/scriptui.ts @@ -1,6 +1,68 @@ import * as io from "./io"; +// if an event is specified, it goes here +export const EVENT_KEY = "$$event"; + +// an object that can become interactive, identified by ID +export interface Interactive { + $$interact: InteractionRecord; +} + +export interface InteractEvent { + interactid : number; + type: string; + x?: number; + y?: number; + button?: boolean; +} + +// InteractionRecord maps a target object to an interaction ID +// the $$callback is used once per script eval, then gets nulled +// whether or not it's invoked +// event comes from $$data.$$event +export class InteractionRecord implements io.Loadable { + interactid : number; + lastevent : {} = null; + constructor( + public readonly interacttarget: Interactive, + private $$callback + ) { + } + $$setstate(newstate: {interactid: number}) { + this.interactid = newstate.interactid; + this.interacttarget.$$interact = this; + let event : InteractEvent = io.data.get(EVENT_KEY); + if (event && event.interactid == this.interactid) { + if (this.$$callback) { + this.$$callback(event); + } + this.lastevent = event; + io.data.set(EVENT_KEY, null); + } + this.$$callback = null; + } + $$getstate() { + //TODO: this isn't always cleared before we serialize (e.g. if exception or move element) + this.$$callback = null; + return this; + } +} + +export function isInteractive(obj: object): obj is Interactive { + return !!((obj as Interactive).$$interact); +} + +export function interact(object: any, callback) : InteractionRecord { + // TODO: limit to Bitmap, etc + if (typeof object === 'object') { + return new InteractionRecord(object, callback); + } + throw new Error(`This object is not capable of interaction.`); +} + +/// + export interface ScriptUIType { uitype : string; } @@ -18,7 +80,6 @@ export class ScriptUISliderType implements ScriptUIType { } export class ScriptUISlider extends ScriptUISliderType implements io.Loadable { - initvalue: number; initial(value: number) { this.value = value; return this; @@ -41,8 +102,8 @@ export class ScriptUISelectType implements ScriptUIType { constructor( readonly options: T[] ) { - this.value = null; - this.index = -1; + this.index = 0; + this.value = this.options[this.index]; } } @@ -60,3 +121,20 @@ export class ScriptUISelect extends ScriptUISelectType implements io.Loada export function select(options: any[]) { return new ScriptUISelect(options); } + +/// + +export class ScriptUIButtonType implements ScriptUIType { + readonly uitype = 'button'; + constructor( + readonly name: string + ) { + } +} + +export class ScriptUIButton extends ScriptUIButtonType { +} + +export function button(name: string) { + return new ScriptUIButton(name); +} diff --git a/src/common/script/ui/notebook.ts b/src/common/script/ui/notebook.ts index 4c57e366..2eb27b5c 100644 --- a/src/common/script/ui/notebook.ts +++ b/src/common/script/ui/notebook.ts @@ -1,14 +1,13 @@ -import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap"; import { Component, render, h, createRef, VNode } from 'preact'; -import { Cell } from "../env"; +import { Cell, PROP_CONSTRUCTOR_NAME } from "../env"; import { findIntegerFactors, hex, isArray, rgb2bgr } from "../../util"; import { dumpRAM } from "../../emu"; -// TODO: can't call methods from this end -import * as color from "../lib/color"; -import { ScriptUISelectType, ScriptUISliderType, ScriptUIType } from "../lib/scriptui"; import { current_project } from "../../../ide/ui"; -import { EVENT_KEY, InteractEvent, Interactive, isInteractive } from "../lib/io"; +// TODO: can't call methods from this end (e.g. Palette, Bitmap) +import * as bitmap from "../lib/bitmap"; +import * as color from "../lib/color"; +import * as scriptui from "../lib/scriptui"; const MAX_STRING_LEN = 100; const DEFAULT_ASPECT = 1; @@ -57,9 +56,9 @@ class ColorComponent extends Component { } } -function sendInteraction(iobj: Interactive, type: string, event: Event, xtraprops: {}) { +function sendInteraction(iobj: scriptui.Interactive, type: string, event: Event, xtraprops: {}) { let irec = iobj.$$interact; - let ievent : InteractEvent = {interactid: irec.interactid, type, ...xtraprops}; + let ievent : scriptui.InteractEvent = {interactid: irec.interactid, type, ...xtraprops}; if (event instanceof PointerEvent) { const canvas = event.target as HTMLCanvasElement; const rect = canvas.getBoundingClientRect(); @@ -73,13 +72,13 @@ function sendInteraction(iobj: Interactive, type: string, event: Event, xtraprop } // TODO: add events to queue? current_project.updateDataItems([{ - key: EVENT_KEY, + key: scriptui.EVENT_KEY, value: ievent }]); } interface BitmapComponentProps { - bitmap: BitmapType; + bitmap: bitmap.BitmapType; width: number; height: number; } @@ -101,9 +100,10 @@ class BitmapComponent extends Component { ref: this.ref, width: this.props.width, height: this.props.height, + style: this.props.bitmap.style } let obj : any = this.props.bitmap; - if (isInteractive(obj)) { + if (scriptui.isInteractive(obj)) { return h('canvas', { onPointerMove: (e: PointerEvent) => { sendInteraction(obj, 'move', e, { pressed: this.pressed }); @@ -111,15 +111,15 @@ class BitmapComponent extends Component { onPointerDown: (e: PointerEvent) => { this.pressed = true; this.canvas.setPointerCapture(e.pointerId); - sendInteraction(obj, 'down', e, { }); + sendInteraction(obj, 'down', e, { pressed: true }); }, onPointerUp: (e: PointerEvent) => { this.pressed = false; - sendInteraction(obj, 'up', e, { }); + sendInteraction(obj, 'up', e, { pressed: false }); }, onPointerOut: (e: PointerEvent) => { this.pressed = false; - sendInteraction(obj, 'out', e, { }); + sendInteraction(obj, 'out', e, { pressed: false }); }, ...props }); @@ -154,18 +154,18 @@ class BitmapComponent extends Component { this.updateCanvas(this.datau32, this.props.bitmap); this.ctx.putImageData(this.imageData, 0, 0); } - updateCanvas(vdata: Uint32Array, bmp: BitmapType) { + updateCanvas(vdata: Uint32Array, bmp: bitmap.BitmapType) { if (bmp['palette']) { - this.updateCanvasIndexed(vdata, bmp as IndexedBitmap); + this.updateCanvasIndexed(vdata, bmp as bitmap.IndexedBitmap); } if (bmp['rgba']) { - this.updateCanvasRGBA(vdata, bmp as RGBABitmap); + this.updateCanvasRGBA(vdata, bmp as bitmap.RGBABitmap); } } - updateCanvasRGBA(vdata: Uint32Array, bmp: RGBABitmap) { + updateCanvasRGBA(vdata: Uint32Array, bmp: bitmap.RGBABitmap) { vdata.set(bmp.rgba); } - updateCanvasIndexed(vdata: Uint32Array, bmp: IndexedBitmap) { + updateCanvasIndexed(vdata: Uint32Array, bmp: bitmap.IndexedBitmap) { let pal = bmp.palette.colors; for (var i = 0; i < bmp.pixels.length; i++) { vdata[i] = pal[bmp.pixels[i]]; @@ -190,7 +190,6 @@ class ObjectKeyValueComponent extends Component this.toggleExpand() : null }, [ - h('span', { class: 'tree-key' }, [ propName, expandable ]), - h('span', { class: 'tree-value scripting-item' }, [ getShortName(this.props.object) ]) + propName != null ? h('span', { class: 'tree-key' }, [ propName, expandable ]) : null, + h('span', { class: 'tree-value scripting-item' }, [ + getShortName(this.props.object) + ]) ]), this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null ]); @@ -213,7 +214,7 @@ class ObjectKeyValueComponent extends Component 0) { s += `[${object.length}]` } @@ -241,18 +242,20 @@ function primitiveToString(obj) { } else if (typeof obj == 'number') { if (obj != (obj | 0)) text = obj.toString(); // must be a float else text = obj + "\t($" + hex(obj) + ")"; + } else if (typeof obj == 'string') { + text = obj; } else { text = JSON.stringify(obj); - if (text.length > MAX_STRING_LEN) - text = text.substring(0, MAX_STRING_LEN) + "..."; } + if (text.length > MAX_STRING_LEN) + text = text.substring(0, MAX_STRING_LEN) + "..."; return text; } -function isIndexedBitmap(object): object is IndexedBitmap { +function isIndexedBitmap(object): object is bitmap.IndexedBitmap { return object['bitsPerPixel'] && object['pixels'] && object['palette']; } -function isRGBABitmap(object): object is RGBABitmap { +function isRGBABitmap(object): object is bitmap.RGBABitmap { return object['rgba'] instanceof Uint32Array; } @@ -269,6 +272,12 @@ function objectToChildren(object: any) : any[] { } function objectToDiv(object: any, name: string, objpath: string): VNode { + // don't view any keys that start with "$" + if (name && name.startsWith("$")) { + // don't view any values that start with "$$" + if (name.startsWith("$$")) { return; } + name = null; + } // TODO: limit # of items // TODO: detect table if (object == null) { @@ -332,12 +341,12 @@ function objectToContentsDiv(object: {} | [], objpath: string) { interface UIComponentProps { iokey: string; - uiobject: ScriptUIType; + uiobject: scriptui.ScriptUIType; } class UISliderComponent extends Component { render(virtualDom, containerNode, replaceNode) { - let slider = this.props.uiobject as ScriptUISliderType; + let slider = this.props.uiobject as scriptui.ScriptUISliderType; return h('div', {}, [ this.props.iokey, h('input', { @@ -350,8 +359,8 @@ class UISliderComponent extends Component { this.setState(this.state); current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]); } - }, []), - getShortName(slider.value) + }), + h('span', { }, getShortName(slider.value)), ]); } } @@ -359,24 +368,26 @@ class UISliderComponent extends Component { class UISelectComponent extends Component { ref = createRef(); render(virtualDom, containerNode, replaceNode) { - let select = this.props.uiobject as ScriptUISelectType; + let select = this.props.uiobject as scriptui.ScriptUISelectType; let children = objectToChildren(select.options); return h('div', { class: 'scripting-select scripting-flex', ref: this.ref, onClick: (e) => { - // iterate parents until we find select div, then find index of child + // select object -- iterate parents until we find select div, then find index of child let target = e.target as HTMLElement; while (target.parentElement && target.parentElement != this.ref.current) { target = target.parentElement; } - const selindex = Array.from(target.parentElement.children).indexOf(target); - if (selindex >= 0 && selindex < children.length) { - let newUIValue = { value: children[selindex], index: selindex }; - this.setState(this.state); - current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]); - } else { - throw new Error(`Could not find click target of ${this.props.iokey}`); + if (target.parentElement) { + const selindex = Array.from(target.parentElement.children).indexOf(target); + if (selindex >= 0 && selindex < children.length) { + let newUIValue = { value: children[selindex], index: selindex }; + this.setState(this.state); + current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]); + } else { + throw new Error(`Could not find click target of ${this.props.iokey}`); + } } } }, diff --git a/src/worker/tools/script.ts b/src/worker/tools/script.ts index b3eb1ae6..5d6f73cc 100644 --- a/src/worker/tools/script.ts +++ b/src/worker/tools/script.ts @@ -23,7 +23,7 @@ export async function runJavascript(step: BuildStep): Promise { io.$$loadData(store.items); // TODO: load from file await env.run(code); let cells = env.render(); - let state = env.getLoadableState(); + let state = env.getLoadableState(); // TODO: doesn't work let output : RunResult = { cells, state }; return { output: output }; } catch (e) {