Skip to content

Commit 9714a78

Browse files
committed
Web - Threads - make Pause a toggle, and set Clear apart
1 parent 2258fcc commit 9714a78

6 files changed

Lines changed: 90 additions & 16 deletions

File tree

‎app/web/app/src/icons.js‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/web/app/src/insight.js‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { animateWhileShown } from "./ui/shown.js";
1111
import { createProcessTree } from "./process-tree.js";
1212
import { createPianoRoll } from "./piano-roll.js";
1313
import { perfAdd } from "./perf.js";
14+
import { icon } from "./icons.js";
15+
import { askTwice } from "./ui/ask-twice.js";
1416

1517
const PALETTE = ["HighlightedBackground", "NumberForeground", "KeywordForeground", "DoubleQuotedStringForeground", "Scope_2", "HoverButton", "LogBackground_2", "SymbolForeground"];
1618
const MAX_EVENTS = 600;
@@ -200,24 +202,37 @@ export function createInsight(root, hooks) {
200202
b.addEventListener("click", () => { windowTarget = secs; [...windows.children].forEach((c) => { c.classList.toggle("active", c === b); c.setAttribute("aria-pressed", String(c === b)); }); });
201203
windows.appendChild(b);
202204
}
203-
// Pause holds every view where it is — the tree's rows, the timeline's clock, the roll — to explore it
205+
// Pause holds every view where it is — the tree's rows, the timeline's clock, the roll — to explore it. A toggle,
206+
// the size of the chips beside it: lit while it holds, its name the same either way
204207
let frozenRows = null;
205-
const pause = el("button", "sp-mini-btn insight-pause", "Pause");
208+
const pauseSeg = el("div", "seg");
209+
const pause = el("button", "insight-pause");
210+
pause.type = "button";
211+
pause.innerHTML = `${icon("player-pause")}Pause`;
212+
pause.setAttribute("aria-pressed", "false");
213+
pause.title = "Hold every view where it is, to look around it";
206214
pause.addEventListener("click", () => {
207215
paused = !paused;
208216
frozenNow = paused ? hooks.now() : null;
209217
frozenRows = paused ? hooks.processes() : null;
210-
pause.textContent = paused ? "Resume" : "Pause";
211-
pause.classList.toggle("on", paused);
218+
pause.classList.toggle("active", paused);
219+
pause.setAttribute("aria-pressed", String(paused));
212220
});
213-
head.appendChild(pause);
214-
const clear = el("button", "sp-mini-btn", "Clear");
221+
pauseSeg.appendChild(pause);
222+
// Clear drops the record every view has drawn, which cannot be brought back: at the far end, after the legend,
223+
// away from the controls that are only ways of looking, quieter than they are, and it asks twice (ui/ask-twice.js)
224+
const clearSeg = el("div", "seg insight-clear-seg");
225+
const clear = el("button", "insight-clear");
226+
clear.type = "button";
227+
clear.innerHTML = `${icon("trash")}<span class="ask-label">Clear</span>`;
228+
clear.title = "Clear what every view has drawn so far: it cannot be brought back";
215229
const clearAll = () => { threads.clear(); lanes.clear(); cues = []; beatAnchor = null; roll.clear(); renderTable(); };
216-
clear.addEventListener("click", clearAll);
230+
askTwice(clear, { label: "Clear", armedLabel: "Clear for good", act: clearAll, settle: 3000 });
231+
clearSeg.appendChild(clear);
217232
const legend = el("span", "insight-legend");
218233
legend.innerHTML = '<i class="lg-note"></i>note <i class="lg-sample"></i>sample <i class="lg-sleep"></i>sleep <i class="lg-wait"></i>sync <i class="lg-cue"></i>cue';
219-
timelineControls.append(windows, clear, legend);
220-
head.appendChild(timelineControls);
234+
timelineControls.append(windows, legend, clearSeg); // Clear only where there is a record to clear: not the tree's
235+
head.append(timelineControls, pauseSeg);
221236

222237
const body = el("div", "insight-body");
223238
const canvasWrap = el("div", "insight-canvas");

‎app/web/app/src/sets-view.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// a new set's name and description, or a set's own, and for a set not showing, Delete. The list draws again whenever
77
// the workspace says something changed; a set's details are left alone while they are being typed.
88
import { icon } from "./icons.js";
9+
import { askTwice } from "./ui/ask-twice.js";
910
import { NAME_MAX, DESCRIPTION_MAX } from "./workspace.js";
1011

1112
const ago = (t) => {
@@ -125,13 +126,11 @@ export function createSetsView({ slot, menu, workspace, toast }) {
125126
// delete: asked twice, in its own place, the second time in the danger colour
126127
const del = el("button", "sv-delete", "Delete");
127128
del.type = "button";
128-
let armed = false;
129-
del.addEventListener("click", () => {
130-
if (!armed) { armed = true; del.textContent = "Delete for good"; del.classList.add("armed"); return; }
129+
askTwice(del, { label: "Delete", armedLabel: "Delete for good", act: () => {
131130
workspace.removeSet(s.id);
132131
toast(`deleted "${s.name}"`);
133132
toList();
134-
});
133+
} });
135134
foot.append(del);
136135
}
137136
const cancel = el("button", "sv-cancel", "Cancel"), save = el("button", "sv-save", s ? "Save" : "Create");

‎app/web/app/src/style.css‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ body.no-metro #link-section { display: none; } /* hidden until the preference
357357
/* sets have a colour of their own, everywhere they show (the chip, the list, the Share panel's set): the theme's
358358
second colour, numbers' in the code and native's second swatch, where a buffer has the accent */
359359
:root { --set-colour: var(--NumberForeground); }
360+
/* the danger colour, for what cannot be brought back (ui/ask-twice.js): the error's, as the error pane's */
361+
:root { --danger: var(--MarkerBackground, var(--HighlightedBackground)); }
360362
.set-slot { display: contents; } /* the chip's place in the tab row (buffer-tabs.js): the chip lays out as one of the row's own */
361363
/* the set showing, named, before the tabs once there is more than one set: it opens the list of sets (main.js paintSetChip) */
362364
.set-chip { margin-right: 10px; } /* the row's two ends: SET and JAM a pair, one size and weight (below) */
@@ -417,8 +419,9 @@ body.no-metro #link-section { display: none; } /* hidden until the preference
417419
@media (hover: hover) { .sv-cancel:hover { color: var(--WindowForeground); background: var(--subtleFill); } }
418420
.sv-save { border: 1px solid var(--set-colour); background: var(--set-colour); color: var(--Background); }
419421
@media (hover: hover) { .sv-save:hover { filter: brightness(1.08); } }
420-
.sv-delete { --danger: var(--MarkerBackground, var(--HighlightedBackground)); margin-right: auto; border: 1px solid transparent; background: none; color: var(--danger); } /* the error's colour, as the error pane's */
421-
.sv-delete.armed { border-color: var(--danger); background: var(--danger); color: var(--accentContrastText); }
422+
.sv-delete { margin-right: auto; border: 1px solid transparent; background: none; color: var(--danger); }
423+
/* asked a second time (ui/ask-twice.js): filled in the danger colour */
424+
.sv-delete.armed, .seg button.insight-clear.armed { border-color: var(--danger); border-style: solid; background: var(--danger); color: var(--accentContrastText); }
422425
#sets-menu :is(button, .sv-input):focus-visible { outline: 2px solid var(--set-colour); outline-offset: 1px; }
423426
.buffer-tab-sep { padding: 0 2px; color: var(--WindowBorder); font: 300 var(--t-small) var(--code-font); pointer-events: none; user-select: none; }
424427
.buffer-tab { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; flex: 1 1 34px; min-width: 0; padding: 7px 4px; border: none; background: none; color: var(--faintText); font: var(--t-body)/1 var(--code-font); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: font-size 0.25s ease 0.1s, padding 0.25s ease 0.1s; }
@@ -1421,6 +1424,16 @@ body[data-drawer="logs"] #logs-pane { display: flex; }
14211424
.insight[data-view="timeline"] .insight-processes { display: none; }
14221425
.insight[data-view="roll"] .insight-body, .insight[data-view="roll"] .insight-processes, .insight[data-view="roll"] .insight-legend { display: none; }
14231426
.insight-timeline-controls { display: contents; }
1427+
/* the head: what to look at, how much of it, and holding it still; at the far end, the legend and Clear (insight.js) */
1428+
.insight-legend, .insight-clear-seg { order: 1; }
1429+
.insight-clear-seg { margin-left: 4px; }
1430+
.insight[data-view="roll"] .insight-clear-seg { margin-left: auto; } /* no legend there to carry it to the end */
1431+
:is(.insight-pause, .insight-clear) { display: inline-flex; align-items: center; gap: 6px; }
1432+
:is(.insight-pause, .insight-clear) .tb-icon { width: 14px; height: 14px; flex: none; }
1433+
/* Clear throws the record away: quieter than the ways of looking, and the danger colour once it is pointed at */
1434+
.seg button.insight-clear { border-style: dashed; color: var(--mutedForeground); }
1435+
.seg button.insight-clear:not(.armed):focus-visible { border-color: var(--danger); color: var(--danger); }
1436+
@media (hover: hover) { .seg button.insight-clear:not(.armed):hover { border-color: var(--danger); color: var(--danger); } }
14241437
.insight-processes { flex: 1; min-height: 0; display: flex; flex-direction: column; }
14251438
.ptree-head { display: flex; align-items: center; gap: 12px; padding: 4px 12px; flex-wrap: wrap; }
14261439
.ptree-stats { display: flex; gap: 14px; flex-wrap: wrap; font: var(--t-tiny) var(--code-font); color: var(--mutedForeground); flex: 1; }

‎app/web/app/src/ui/ask-twice.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// A button that asks twice, for what cannot be brought back (deleting a set, clearing the Threads record): the first
3+
// press arms it, its second label in the danger colour (style.css .armed), and the second press does it. Left armed it
4+
// settles back after `settle` ms, or as soon as focus leaves it; with no `settle` it stays armed until pressed again.
5+
// The label is the button's .ask-label where it has one (beside an icon), else its whole text.
6+
export function askTwice(button, { label, armedLabel, act, settle = null }) {
7+
const text = button.querySelector(".ask-label") ?? button;
8+
let timer = null;
9+
const disarm = () => {
10+
clearTimeout(timer);
11+
timer = null;
12+
button.classList.remove("armed");
13+
text.textContent = label;
14+
};
15+
button.addEventListener("click", () => {
16+
if (!button.classList.contains("armed")) {
17+
button.classList.add("armed");
18+
text.textContent = armedLabel;
19+
if (settle != null) timer = setTimeout(disarm, settle);
20+
return;
21+
}
22+
disarm();
23+
act();
24+
});
25+
if (settle != null) button.addEventListener("blur", disarm);
26+
}

‎app/web/scripts/browser-check.mjs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,10 @@ for (const [name, engineType] of [["chromium", chromium], ["webkit", webkit]]) {
465465
const tipText = (await page.locator(".roll-tip:not([hidden])").textContent().catch(() => "")) ?? "";
466466
check("pointing at a note names it, its synth and its line", /^[A-G][♯♭]?\d \(\d+\) · :tb303 · amp 1 · 0\.2 s · :bass · line 7$/.test(tipText), tipText);
467467
await page.mouse.move(0, 0);
468-
await page.locator(".insight-pause", { hasText: "Resume" }).click();
468+
// Pause is a toggle: pressed while it holds, its name the same either way
469+
const pauseHeld = await pause.evaluate((b) => ({ pressed: b.getAttribute("aria-pressed"), text: b.textContent.trim() }));
470+
check("Pause is a toggle, pressed while it holds and named the same", pauseHeld.pressed === "true" && pauseHeld.text === "Pause", JSON.stringify(pauseHeld));
471+
await pause.click();
469472
// the roll has notes still to sound (the bass is a second ahead) once it has drawn again after the resume: waited
470473
// for, not a fixed pause, which a slow machine's drawing can outlast
471474
const aheadNow = () => { const now = window.sonicPi.session.clockNow(); return window.sonicPi.pianoRoll().notes.filter((n) => n.start > now).length; };
@@ -477,6 +480,23 @@ for (const [name, engineType] of [["chromium", chromium], ["webkit", webkit]]) {
477480
// none ahead: say what the roll had, and when, for a machine where it fails
478481
const rollSaid = ahead > 0 ? "" : ` (stopped at ${stoppedAt}: the roll had ${rollStopped.notes.length} notes, the latest starting ${Math.max(...rollStopped.notes.map((n) => n.start))})`;
479482
check("Stop takes the notes that will now never sound off the piano roll", ahead > 0 && rollStopped.notes.every((n) => n.start <= stoppedAt && n.end <= stoppedAt + 1e-6) && rollStopped.hits.every((h) => h.time <= stoppedAt), `${ahead} ahead before, ${rollStopped.notes.filter((n) => n.start > stoppedAt).length} after the stop${rollSaid}`);
483+
// Clear drops the record and cannot bring it back, so it asks twice (ui/ask-twice.js): the first press arms it,
484+
// left alone it settles back with the record kept, and only a second press clears
485+
{
486+
const clearBtn = page.locator(".insight-clear");
487+
const rows = () => page.locator(".insight-table tbody .insight-chip").count(); // a thread's row; not the empty table's line
488+
const state = () => clearBtn.evaluate((b) => ({ armed: b.classList.contains("armed"), text: b.textContent.trim() }));
489+
const before = await rows();
490+
await clearBtn.click();
491+
const armed = await state(), keptArmed = await rows();
492+
await page.waitForTimeout(3300); // a finished thread may leave the table meanwhile, as it would anyway
493+
const settled = await state(), keptSettled = await rows();
494+
// the two presses and the count in one task: a record arriving a moment later is the next one's, not the cleared
495+
const after = await clearBtn.evaluate((b) => { b.click(); b.click(); return b.getRootNode().querySelectorAll(".insight-table tbody .insight-chip").length; });
496+
check("Clear asks twice: armed by a press, settled back if left, the record gone only with a second press",
497+
before > 0 && armed.armed && armed.text === "Clear for good" && keptArmed === before && !settled.armed && settled.text === "Clear" && keptSettled > 0 && after === 0,
498+
`rows ${before} → armed ${keptArmed} (${armed.text}) → settled ${keptSettled} (${settled.text}) → ${after}`);
499+
}
480500
await setCode("n = play 60, release: 2\nsleep 0.25\ncontrol n, note: 72\nset :answer, 42\nputs get(:answer)\nputs spread(3, 8)\nmidi_note_on 60\nputs chord_invert(chord(:c4, :major), 1)");
481501
await page.click("#btn-run");
482502
await page.waitForFunction(() => /\(ring 64, 67, 72\)/.test(document.getElementById("log").shadowRoot.textContent), null, { timeout: 10000 }).catch(() => {});

0 commit comments

Comments
 (0)