From 29dab578e7488f254cbe20105f9e406c1b58f6b1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 14 Mar 2024 23:49:38 +0100 Subject: [PATCH] can now load claviature as a codemirror widget --- packages/codemirror/codemirror.mjs | 9 ++- packages/codemirror/index.mjs | 1 + packages/codemirror/slider.mjs | 38 ++++++------ packages/codemirror/widget.mjs | 95 ++++++++++++++++++++++++++++++ packages/transpiler/transpiler.mjs | 32 +++++++++- 5 files changed, 152 insertions(+), 23 deletions(-) create mode 100644 packages/codemirror/widget.mjs diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 512c873a..c06ac4a0 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -20,7 +20,8 @@ import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs'; -import { updateWidgets, sliderPlugin } from './slider.mjs'; +import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; +import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; const extensions = { @@ -72,6 +73,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo ...initialSettings, javascript(), sliderPlugin, + widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything closeBrackets(), @@ -187,7 +189,10 @@ export class StrudelMirror { // remember for when highlighting is toggled on this.miniLocations = options.meta?.miniLocations; this.widgets = options.meta?.widgets; - updateWidgets(this.editor, this.widgets); + const sliders = this.widgets.filter((w) => w.type === 'slider'); + updateSliderWidgets(this.editor, sliders); + const widgets = this.widgets.filter((w) => w.type !== 'slider'); + updateWidgets(this.editor, widgets); updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); this.adjustDrawTime(); diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 8f2d1630..3a5f2a23 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -3,3 +3,4 @@ export * from './highlight.mjs'; export * from './flash.mjs'; export * from './slider.mjs'; export * from './themes.mjs'; +export * from './widget.mjs'; diff --git a/packages/codemirror/slider.mjs b/packages/codemirror/slider.mjs index a46f0e1e..21f744bb 100644 --- a/packages/codemirror/slider.mjs +++ b/packages/codemirror/slider.mjs @@ -1,10 +1,27 @@ import { ref, pure } from '@strudel/core'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; -import { StateEffect, StateField } from '@codemirror/state'; +import { StateEffect } from '@codemirror/state'; + +export const setSliderWidgets = StateEffect.define(); + +export const updateSliderWidgets = (view, widgets) => { + view.dispatch({ effects: setSliderWidgets.of(widgets) }); +}; export let sliderValues = {}; const getSliderID = (from) => `slider_${from}`; +function getSliders(widgetConfigs, view) { + return widgetConfigs + .filter((w) => w.type === 'slider') + .map(({ from, to, value, min, max, step }) => { + return Decoration.widget({ + widget: new SliderWidget(value, min, max, from, to, step, view), + side: 0, + }).range(from /* , to */); + }); +} + export class SliderWidget extends WidgetType { constructor(value, min, max, from, to, step, view) { super(); @@ -60,21 +77,6 @@ export class SliderWidget extends WidgetType { } } -export const setWidgets = StateEffect.define(); - -export const updateWidgets = (view, widgets) => { - view.dispatch({ effects: setWidgets.of(widgets) }); -}; - -function getWidgets(widgetConfigs, view) { - return widgetConfigs.map(({ from, to, value, min, max, step }) => { - return Decoration.widget({ - widget: new SliderWidget(value, min, max, from, to, step, view), - side: 0, - }).range(from /* , to */); - }); -} - export const sliderPlugin = ViewPlugin.fromClass( class { decorations; //: DecorationSet @@ -99,8 +101,8 @@ export const sliderPlugin = ViewPlugin.fromClass( } } for (let e of tr.effects) { - if (e.is(setWidgets)) { - this.decorations = Decoration.set(getWidgets(e.value, update.view)); + if (e.is(setSliderWidgets)) { + this.decorations = Decoration.set(getSliders(e.value, update.view)); } } }); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs new file mode 100644 index 00000000..3df38d8b --- /dev/null +++ b/packages/codemirror/widget.mjs @@ -0,0 +1,95 @@ +import { StateEffect, StateField } from '@codemirror/state'; +import { Decoration, EditorView, WidgetType } from '@codemirror/view'; +import { Pattern } from '@strudel/core'; + +const getWidgetID = (from) => `ui_${from}`; + +Pattern.prototype.ui = function (id, value) { + // TODO: make this work with any web component + return this.onFrame((haps) => { + let el = document.getElementById(id); + if (el) { + let options = {}; + const keys = haps.map((h) => h.value.note); + el.setAttribute( + 'options', + JSON.stringify({ + ...options, + range: options.range || ['A2', 'C6'], + colorize: [{ keys: keys, color: options.color || 'steelblue' }], + }), + ); + } + }); +}; + +export const addWidget = StateEffect.define({ + map: ({ from, to }, change) => { + return { from: change.mapPos(from), to: change.mapPos(to) }; + }, +}); + +export const updateWidgets = (view, widgets) => { + view.dispatch({ effects: addWidget.of(widgets) }); +}; + +function getWidgets(widgetConfigs, view) { + return widgetConfigs.map(({ from, to }) => { + return Decoration.widget({ + widget: new BlockWidget(view, from), + side: 0, + block: true, + }).range(to); + }); +} + +const widgetField = StateField.define( + /* */ { + create() { + return Decoration.none; + }, + update(widgets, tr) { + widgets = widgets.map(tr.changes); + for (let e of tr.effects) { + if (e.is(addWidget)) { + try { + widgets = widgets.update({ + filter: () => false, + add: getWidgets(e.value), + }); + } catch (error) { + console.log('err', error); + } + } + } + return widgets; + }, + provide: (f) => EditorView.decorations.from(f), + }, +); + +export class BlockWidget extends WidgetType { + constructor(view, from) { + super(); + this.view = view; + this.from = from; + } + eq() { + return true; + } + toDOM() { + const id = getWidgetID(this.from); // matches id generated in transpiler + let el = document.getElementById(id); + if (!el) { + // TODO: make this work with any web component + el = document.createElement('strudel-claviature'); + el.id = id; + } + return el; + } + ignoreEvent(e) { + return true; + } +} + +export const widgetPlugin = [widgetField]; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 72f2e851..48db38a6 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -34,7 +34,7 @@ export function transpiler(input, options = {}) { emitMiniLocations && collectMiniLocations(value, node); return this.replace(miniWithLocation(value, node)); } - if (isWidgetFunction(node)) { + if (isSliderFunction(node)) { emitWidgets && widgets.push({ from: node.arguments[0].start, @@ -43,6 +43,16 @@ export function transpiler(input, options = {}) { min: node.arguments[1]?.value ?? 0, max: node.arguments[2]?.value ?? 1, step: node.arguments[3]?.value, + type: 'slider', + }); + return this.replace(sliderWithLocation(node)); + } + if (isUIFunction(node)) { + emitWidgets && + widgets.push({ + from: node.arguments[0].start, + to: node.end, + type: node.arguments[0].value, }); return this.replace(widgetWithLocation(node)); } @@ -105,11 +115,15 @@ function miniWithLocation(value, node) { // these functions are connected to @strudel/codemirror -> slider.mjs // maybe someday there will be pluggable transpiler functions, then move this there -function isWidgetFunction(node) { +function isSliderFunction(node) { return node.type === 'CallExpression' && node.callee.name === 'slider'; } -function widgetWithLocation(node) { +function isUIFunction(node) { + return node.type === 'CallExpression' && node.callee.property?.name === 'ui'; +} + +function sliderWithLocation(node) { const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id // add loc as identifier to first argument // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) @@ -122,6 +136,18 @@ function widgetWithLocation(node) { return node; } +function widgetWithLocation(node) { + const id = 'ui_' + node.arguments[0].start; // use loc of first arg for id + // add loc as identifier to first argument + // the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?) + node.arguments.unshift({ + type: 'Literal', + value: id, + raw: id, + }); + return node; +} + function isBareSamplesCall(node, parent) { return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression'; }