mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-16 08:08:34 +00:00
make sliders work!
This commit is contained in:
parent
b36cee93f7
commit
062d926900
@ -1,6 +1,8 @@
|
||||
import { WidgetType } from '@codemirror/view';
|
||||
import { ViewPlugin, Decoration } from '@codemirror/view';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
|
||||
export let sliderValues = {};
|
||||
const getSliderID = (from) => `slider_${from}`;
|
||||
|
||||
export class SliderWidget extends WidgetType {
|
||||
constructor(value, min, max, from, to) {
|
||||
@ -9,17 +11,12 @@ export class SliderWidget extends WidgetType {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.from = from;
|
||||
this.originalFrom = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
eq(other) {
|
||||
const isSame =
|
||||
other.value.toFixed(4) == this.value.toFixed(4) &&
|
||||
other.min == this.min &&
|
||||
other.max == this.max &&
|
||||
other.from === this.from &&
|
||||
other.to === this.to;
|
||||
return isSame;
|
||||
eq() {
|
||||
return false;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
@ -31,10 +28,15 @@ export class SliderWidget extends WidgetType {
|
||||
slider.min = this.min;
|
||||
slider.max = this.max;
|
||||
slider.step = (this.max - this.min) / 1000;
|
||||
slider.value = this.value;
|
||||
slider.originalValue = this.value.toFixed(2);
|
||||
// to make sure the code stays in sync, let's save the original value
|
||||
// becuase .value automatically clamps values so it'll desync with the code
|
||||
slider.value = slider.originalValue;
|
||||
slider.from = this.from;
|
||||
slider.originalFrom = this.originalFrom;
|
||||
slider.to = this.to;
|
||||
slider.className = 'w-16 translate-y-1.5';
|
||||
slider.className = 'w-16 translate-y-1.5 mx-2';
|
||||
this.slider = slider;
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@ -43,78 +45,50 @@ export class SliderWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
let nodeValue = (node, view) => view.state.doc.sliceString(node.from, node.to);
|
||||
export const setWidgets = StateEffect.define();
|
||||
|
||||
// matches a number and returns slider widget
|
||||
/* let matchNumber = (node, view) => {
|
||||
if (node.name == 'Number') {
|
||||
const value = view.state.doc.sliceString(node.from, node.to);
|
||||
let min = 0;
|
||||
let max = 10;
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(Number(value), min, max, node.from, node.to),
|
||||
side: 0,
|
||||
});
|
||||
}
|
||||
}; */
|
||||
|
||||
// matches something like slider(123) and returns slider widget
|
||||
let matchSliderFunction = (node, view) => {
|
||||
if (node.name === 'CallExpression' /* && node.node.firstChild.name === 'ArgList' */) {
|
||||
let name = nodeValue(node.node.firstChild, view); // slider ?
|
||||
if (name === 'slider') {
|
||||
const args = node.node.lastChild.getChildren('Number');
|
||||
if (!args.length) {
|
||||
return;
|
||||
}
|
||||
const [value, min = 0, max = 1] = args.map((node) => nodeValue(node, view));
|
||||
//console.log('slider value', value, min, max);
|
||||
let { from, to } = args[0];
|
||||
let widget = Decoration.widget({
|
||||
widget: new SliderWidget(Number(value), min, max, from, to),
|
||||
side: 0,
|
||||
});
|
||||
//widget._range = widget.range(from);
|
||||
widget._range = widget.range(node.from);
|
||||
return widget;
|
||||
}
|
||||
// node is sth like 123.xxx
|
||||
}
|
||||
export const updateWidgets = (view, widgets) => {
|
||||
view.dispatch({ effects: setWidgets.of(widgets) });
|
||||
};
|
||||
|
||||
// EditorView
|
||||
export function sliders(view) {
|
||||
let widgets = [];
|
||||
for (let { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: (node) => {
|
||||
let widget = matchSliderFunction(node, view);
|
||||
// let widget = matchNumber(node, view);
|
||||
if (widget) {
|
||||
widgets.push(widget._range || widget.range(node.from));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return Decoration.set(widgets);
|
||||
let draggedSlider;
|
||||
|
||||
function getWidgets(widgetConfigs) {
|
||||
return widgetConfigs.map(({ from, to, value, min, max }) => {
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(Number(value), min, max, from, to),
|
||||
side: 0,
|
||||
}).range(from /* , to */);
|
||||
});
|
||||
}
|
||||
|
||||
let draggedSlider, init;
|
||||
export const sliderPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations; //: DecorationSet
|
||||
|
||||
constructor(view /* : EditorView */) {
|
||||
this.decorations = sliders(view);
|
||||
this.decorations = Decoration.set([]);
|
||||
}
|
||||
|
||||
update(update /* : ViewUpdate */) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
!init && (this.decorations = sliders(update.view));
|
||||
//init = true;
|
||||
}
|
||||
update.transactions.forEach((tr) => {
|
||||
if (tr.docChanged) {
|
||||
this.decorations = this.decorations.map(tr.changes);
|
||||
const iterator = this.decorations.iter();
|
||||
while (iterator.value) {
|
||||
// when the widgets are moved, we need to tell the dom node the current position
|
||||
// this is important because the updateSliderValue function has to work with the dom node
|
||||
iterator.value.widget.slider.from = iterator.from;
|
||||
iterator.value.widget.slider.to = iterator.to;
|
||||
iterator.next();
|
||||
}
|
||||
}
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setWidgets)) {
|
||||
this.decorations = Decoration.set(getWidgets(e.value));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -124,6 +98,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
||||
mousedown: (e, view) => {
|
||||
let target = e.target; /* as HTMLElement */
|
||||
if (target.nodeName == 'INPUT' && target.parentElement.classList.contains('cm-slider')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
draggedSlider = target;
|
||||
// remember offsetLeft / clientWidth, as they will vanish inside mousemove events for some reason
|
||||
draggedSlider._offsetLeft = draggedSlider.offsetLeft;
|
||||
@ -141,6 +117,7 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
||||
},
|
||||
);
|
||||
|
||||
// moves slider on mouse event
|
||||
function updateSliderValue(view, e) {
|
||||
const mouseX = e.clientX;
|
||||
let progress = (mouseX - draggedSlider._offsetLeft) / draggedSlider._clientWidth;
|
||||
@ -149,30 +126,38 @@ function updateSliderValue(view, e) {
|
||||
let max = Number(draggedSlider.max);
|
||||
const next = Number(progress * (max - min) + min);
|
||||
let insert = next.toFixed(2);
|
||||
let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim();
|
||||
before = Number(before).toFixed(4);
|
||||
if (before === next) {
|
||||
//let before = view.state.doc.sliceString(draggedSlider.from, draggedSlider.to).trim();
|
||||
let before = draggedSlider.originalValue;
|
||||
before = Number(before).toFixed(2);
|
||||
// console.log('before', before, 'insert', insert, 'v');
|
||||
if (before === insert) {
|
||||
return false;
|
||||
}
|
||||
let change = { from: draggedSlider.from, to: draggedSlider.to, insert };
|
||||
draggedSlider.to = draggedSlider.from + insert.length;
|
||||
const to = draggedSlider.from + before.length;
|
||||
let change = { from: draggedSlider.from, to, insert };
|
||||
draggedSlider.originalValue = insert;
|
||||
draggedSlider.value = insert;
|
||||
view.dispatch({ changes: change });
|
||||
const id = 'slider_' + draggedSlider.from; // matches id generated in transpiler
|
||||
const id = getSliderID(draggedSlider.originalFrom); // matches id generated in transpiler
|
||||
window.postMessage({ type: 'cm-slider', value: next, id });
|
||||
return true;
|
||||
}
|
||||
|
||||
export let sliderValues = {};
|
||||
|
||||
// user api
|
||||
export let slider = (id, value, min, max) => {
|
||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||
return ref(() => sliderValues[id]); // use state at query time
|
||||
};
|
||||
// update state when sliders are moved
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data.type === 'cm-slider') {
|
||||
// update state when slider is moved
|
||||
sliderValues[e.data.id] = e.data.value;
|
||||
if (sliderValues[e.data.id] !== undefined) {
|
||||
// update state when slider is moved
|
||||
sliderValues[e.data.id] = e.data.value;
|
||||
} else {
|
||||
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
13
packages/react/src/hooks/useWidgets.mjs
Normal file
13
packages/react/src/hooks/useWidgets.mjs
Normal file
@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { updateWidgets } from '@strudel/codemirror';
|
||||
|
||||
// i know this is ugly.. in the future, repl needs to run without react
|
||||
export function useWidgets(view) {
|
||||
const [widgets, setWidgets] = useState([]);
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
updateWidgets(view, widgets);
|
||||
}
|
||||
}, [view, widgets]);
|
||||
return { widgets, setWidgets };
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core';
|
||||
import { getLeafLocations } from '@strudel.cycles/mini';
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
let ast = parse(input, {
|
||||
ecmaVersion: 2022,
|
||||
@ -16,9 +16,9 @@ export function transpiler(input, options = {}) {
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
|
||||
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
};
|
||||
let widgets = [];
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
@ -37,6 +37,14 @@ export function transpiler(input, options = {}) {
|
||||
}
|
||||
if (isWidgetFunction(node)) {
|
||||
// collectSliderLocations?
|
||||
emitWidgets &&
|
||||
widgets.push({
|
||||
from: node.arguments[0].start,
|
||||
to: node.arguments[0].end,
|
||||
value: node.arguments[0].value,
|
||||
min: node.arguments[1]?.value ?? 0,
|
||||
max: node.arguments[2]?.value ?? 1,
|
||||
});
|
||||
return this.replace(widgetWithLocation(node));
|
||||
}
|
||||
// TODO: remove pseudo note variables?
|
||||
@ -68,7 +76,7 @@ export function transpiler(input, options = {}) {
|
||||
if (!emitMiniLocations) {
|
||||
return { output };
|
||||
}
|
||||
return { output, miniLocations };
|
||||
return { output, miniLocations, widgets };
|
||||
}
|
||||
|
||||
function isStringWithDoubleQuotes(node, locations, code) {
|
||||
|
||||
@ -22,6 +22,7 @@ import Loader from './Loader';
|
||||
import { settingPatterns } from '../settings.mjs';
|
||||
import { code2hash, hash2code } from './helpers.mjs';
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
@ -129,7 +130,7 @@ export function Repl({ embedded = false }) {
|
||||
} = useSettings();
|
||||
|
||||
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
|
||||
|
||||
const { setWidgets } = useWidgets(view);
|
||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||
useStrudel({
|
||||
initialCode: '// LOADING...',
|
||||
@ -143,6 +144,7 @@ export function Repl({ embedded = false }) {
|
||||
},
|
||||
afterEval: ({ code, meta }) => {
|
||||
setMiniLocations(meta.miniLocations);
|
||||
setWidgets(meta.widgets);
|
||||
setPending(false);
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
@ -220,7 +222,7 @@ export function Repl({ embedded = false }) {
|
||||
const handleChangeCode = useCallback(
|
||||
(c) => {
|
||||
setCode(c);
|
||||
started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||
//started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||
},
|
||||
[started],
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user