Skip to content

Commit f620873

Browse files
committed
Web - Editor - float the foot over the code's last lines, with the code padded so its last line scrolls clear
1 parent c0144dc commit f620873

4 files changed

Lines changed: 68 additions & 7 deletions

File tree

‎app/web/app/src/editor.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@ export function createEditor(mount, { api, hooks, workspace }) {
698698

699699
let nudgeSlider = () => false; // slider mode's keyboard step (completion/cm.js), once the completion is set up
700700
const extensions = [
701+
// The foot floats over the code's last lines (style.css #editor-foot, its height in --editor-foot-h on the
702+
// column): the caret is kept above it when the editor scrolls it into view
703+
EditorView.scrollMargins.of(() => ({ bottom: parseFloat(getComputedStyle(mount).getPropertyValue("--editor-foot-h")) || 0 })),
701704
// Native's shortcuts, in whichever keymap the player picked, are main.js's
702705
// (shortcuts.js), and run the editor's commands through command() below.
703706
// These are the keys every keymap leaves to the editor.

‎app/web/app/src/main.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,6 +2133,15 @@ const placeInfo = () => {
21332133
let lastStripH = 0;
21342134
window.addEventListener("resize", () => { if (infoOpen()) placeInfo(); });
21352135
{ const ro = new ResizeObserver(() => { if (infoOpen()) placeInfo(); }); ro.observe(siteNav); ro.observe($("site-strip")); } // the bar grows a row on a phone once its tabs are in
2136+
// The editor's foot floats over the code (style.css #editor-foot): the code is padded by the foot's height, as it
2137+
// stands — a phone's row, focus mode's none — so its last line can always scroll clear of the buffers
2138+
{
2139+
const foot = $("editor-foot"), column = $("editor-column");
2140+
const fit = () => column.style.setProperty("--editor-foot-h", `${foot.offsetHeight}px`);
2141+
new ResizeObserver(fit).observe(foot);
2142+
new MutationObserver(fit).observe(document.body, { attributes: true, attributeFilter: ["class"] }); // focus mode and the keyboard hide it
2143+
fit();
2144+
}
21362145
setInterval(() => { if (infoOpen()) placeInfo(); }, 500); // and, belt and braces, the pages check where the bar ends
21372146
/** The card at a page (the one on show, if none) or at the page holding an id; history: how the address follows
21382147
* ("push" a step Back undoes, "replace" in place of an old link, "none" when the address is where Back went). */

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -316,31 +316,44 @@ body[data-drawer]:not([data-drawer=""]) #main { grid-template-rows: minmax(80px,
316316
body.zen #main { grid-template-columns: 1fr 0 0; }
317317
body.zen #sidebar, body.zen #panel-divider { display: none; }
318318

319-
#editor-column { grid-area: editor; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--Background); }
319+
#editor-column { grid-area: editor; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--Background); position: relative; --editor-foot-h: 48px; } /* the foot floats over the code's last lines (below); its height is measured into --editor-foot-h (main.js) */
320320
.shadow-pane { display: contents; } /* a part's insides, in its shadow root, laid out as its host's own children (shadow.js) */
321321
#editor-mount { flex: 1; min-height: 0; overflow: visible; position: relative; } /* not hidden: the editor inside fills it and scrolls itself, and Safari's completion popup is positioned absolutely within it (CodeMirror's Safari fallback), so a clip here would cut the popup at the editor's foot */
322322
:host(#editor-mount) .cm-scroller, .qs-card-body { overscroll-behavior: none; } /* code stops at its edges: no rubber band past them (iOS) — long lines still scroll, never wrap */
323323
/* the editor's insides are in a shadow root on its mount (shadow.js): its rules name the mount as their :host */
324324
:host(#editor-mount) .cm-editor { height: 100%; }
325325
:host(#editor-mount) .cm-editor.cm-focused { outline: none; }
326326

327-
/* Under the editor: where the caret is, and the ten buffers */
327+
/* Over the foot of the editor: where the caret is, and the ten buffers. It floats on the code's last lines,
328+
transparent, rather than taking a row of its own, and the code is padded by its height (--editor-foot-h) so the
329+
last line can always scroll clear of it. */
328330
#editor-foot {
329331
display: grid;
330332
grid-template-columns: 1fr auto 1fr;
331333
align-items: center;
332334
gap: 12px;
333335
padding: 6px 14px 8px;
334336
flex-shrink: 0;
335-
}
336-
#caret-pos { font: var(--t-small) var(--code-font); color: var(--mutedForeground); white-space: pre; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* small and quiet: where the caret is, not what to read */
337+
position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
338+
background: transparent; /* the code shows through whole: the chips carry their own pills and borders */
339+
pointer-events: none; /* the gaps between the chips are the code's: a click there reaches it */
340+
}
341+
#editor-foot > * { pointer-events: auto; }
342+
:host(#editor-mount) .cm-content { padding-bottom: var(--editor-foot-h, 48px); } /* the room under the code for the foot */
343+
#error-strip:not([hidden]) { margin-bottom: var(--editor-foot-h, 48px); } /* the strip, when shown, sits above the foot, not under it */
344+
#caret-pos { font: var(--t-small) var(--code-font); color: var(--mutedForeground); white-space: pre; overflow: hidden; text-overflow: ellipsis; min-width: 0; justify-self: start; padding: 2px 8px; border-radius: var(--r-pill); background: color-mix(in srgb, var(--Background) 90%, transparent); } /* a solid pill, as the chips: readable over the code the foot floats on (a blur of code text smears) */ /* small and quiet: where the caret is, not what to read */
337345
body.no-caret-pos #caret-pos { visibility: hidden; } /* hidden until the preference shows it (main.js); its room is kept, so the buffer pads stay centred */
338346
body.no-metro #link-section { display: none; } /* hidden until the preference shows it (main.js) */
339347
/* The buffer tabs, in the theme's colours. Pads 0-9 between thin bars, each a small pill behind its digit, the
340348
active one filled; one with code in it brighter than an empty one. The jam toggle at the row's end; with jam on
341349
the pads grow to drum-pad size and the run / show / exec picker sits above them. */
342350
#buffer-tabs { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }
343351
.buffer-tabs-row { display: flex; align-items: center; justify-content: center; min-width: 0; }
352+
/* the ten digits as one solid capsule, like the chips beside it: each digit's own pill is the pane's colour, which
353+
over the code the foot floats on is no pill at all, and the code read between the numbers. Solid, not frosted:
354+
a blur of the code's bright text behind came out as milky smears wherever there was text, and nothing elsewhere */
355+
#buffer-tabs:not(.jam-on) .buffer-tabs-list { display: flex; align-items: center; flex: 1 1 auto; min-width: 0; border-radius: var(--r-pill); padding: 0 6px; background: color-mix(in srgb, var(--Background) 90%, transparent); } /* a box of its own here (elsewhere display: contents, above): the capsule needs one; jam's square pads are frosted each */
356+
#buffer-tabs:not(.jam-on) .buffer-tab:not(.active):not(:hover)::before { background: transparent; } /* one surface: a digit's own pill would be a box in the box; the active buffer alone keeps its accent */
344357
/* sets have a colour of their own, everywhere they show (the chip, the list, the Share panel's set): the theme's
345358
second colour, numbers' in the code and native's second swatch, where a buffer has the accent */
346359
:root { --set-colour: var(--NumberForeground); }
@@ -423,23 +436,23 @@ body.no-metro #link-section { display: none; } /* hidden until the preference
423436
/* the jam toggle: a quiet pill, filled and glowing when on */
424437
/* SET and JAM, the row's two ends: one pill, the same size in every layout and state (a width that holds JAM's grid
425438
mark too), the same words' weight; what they say apart is only their colour: SET's the sets', JAM's the accent */
426-
:is(.set-chip, .jam-toggle) { flex: none; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; width: 64px; height: 26px; margin-block: -3px; padding: 0 8px; border: 1px solid var(--WindowBorder); border-radius: var(--r-pill); background: none; color: var(--mutedForeground); font: 600 var(--t-label)/1 var(--prose-font); letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background 0.18s, color 0.18s, border-color 0.18s; }
439+
:is(.set-chip, .jam-toggle) { flex: none; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; width: 64px; height: 26px; margin-block: -3px; padding: 0 8px; border: 1px solid var(--WindowBorder); border-radius: var(--r-pill); background: color-mix(in srgb, var(--Background) 90%, transparent); color: var(--mutedForeground); font: 600 var(--t-label)/1 var(--prose-font); letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background 0.18s, color 0.18s, border-color 0.18s; }
427440
.jam-toggle { margin-left: 10px; }
428441
/* jam on: the pair in the accent together, framing the pads; the set's list open, a shade brighter */
429442
:is(.jam-toggle.on, #buffer-tabs.jam-on .set-chip) { border-color: var(--HighlightedBackground); background: var(--HighlightedBackground); color: var(--accentContrastText); }
430443
#buffer-tabs.jam-on .set-chip[aria-expanded="true"] { filter: brightness(1.12); }
431444
#buffer-tabs.jam-on .set-chip:focus-visible { outline: 2px solid var(--HighlightedBackground); outline-offset: 2px; } /* the keyboard's ring, outside the fill */
432445
@media (hover: hover) { :is(#buffer-tabs:not(.jam-on) .set-chip, .jam-toggle:not(.on)):hover { border-color: var(--HoverButton); color: var(--HoverButton); } } /* the pair's hover, one */
433446
/* the mode picker: run | show | exec, only while jam is on */
434-
.jam-modes { display: none; overflow: hidden; border: 1px solid var(--HighlightedBackground); border-radius: var(--r-s); }
447+
.jam-modes { display: none; overflow: hidden; border: 1px solid var(--HighlightedBackground); border-radius: var(--r-s); background: color-mix(in srgb, var(--Background) 90%, transparent); } /* solid, as the chips beside it */
435448
#buffer-tabs.jam-on .jam-modes { display: inline-flex; }
436449
.jam-mode { min-width: 40px; padding: 5px 8px; border: none; border-right: 1px solid var(--HighlightedBackground); background: none; color: var(--HighlightedBackground); font: 600 var(--t-label)/1 var(--prose-font); letter-spacing: 0.04em; text-transform: uppercase; cursor: pointer; }
437450
.jam-mode:last-child { border-right: none; }
438451
.jam-mode.on { background: var(--HighlightedBackground); color: var(--accentContrastText); }
439452
/* jam on: drum pads — big, bordered, the active one filled, a flash on every hit */
440453
#buffer-tabs.jam-on .buffer-tab { flex: none; width: 58px; padding: 26px 0; color: var(--HighlightedBackground); font: 500 24px/1 var(--code-font); } /* square pads */
441454
#buffer-tabs.jam-on .buffer-tab.active { color: var(--accentContrastText); }
442-
#buffer-tabs.jam-on .buffer-tab::before { width: min(52px, calc(100% - 6px)); height: auto; aspect-ratio: 1; border: 2px solid var(--HighlightedBackground); border-radius: var(--r-m); background: transparent; box-sizing: border-box; }
455+
#buffer-tabs.jam-on .buffer-tab::before { width: min(52px, calc(100% - 6px)); height: auto; aspect-ratio: 1; border: 2px solid var(--HighlightedBackground); border-radius: var(--r-m); background: color-mix(in srgb, var(--Background) 90%, transparent); /* solid, as the chips: the pad floats on the code */ box-sizing: border-box; }
443456
#buffer-tabs.jam-on .buffer-tab.active::before { background: var(--HighlightedBackground); }
444457
#buffer-tabs.jam-on .buffer-tab-sep { display: none; }
445458
/* jam on: the row a grid, the modes right against the pads, no room between (buffer-tabs.js puts them in the row). Two

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,39 @@ const stillness = (pg, ms = 2500) => pg.evaluate((ms) => new Promise((resolve) =
4141
const still = (heard) => !Object.keys(heard).length;
4242
// what only one engine can be asked: said with its reason, and counted apart from what passed
4343
const skip = (name, why) => { results.push({ engine, name, ok: true, skipped: true }); say("skip", name, why); };
44+
// The editor's foot (the caret readout, the buffers) floats over the code's last lines rather than taking a row of its
45+
// own (style.css #editor-foot): the code is padded by the foot's measured height (main.js --editor-foot-h), so its last
46+
// line, and the caret on it, scroll clear of the foot, and a tap in the foot's gaps reaches the code. Asked of a long
47+
// program with the caret at its end; the buffer is put back as it was.
48+
const footFloats = async (pg, where) => {
49+
const r = await pg.evaluate(async () => {
50+
const ed = window.sonicPi.editor, view = ed.view;
51+
const before = view.state.doc.toString();
52+
ed.setCode(Array.from({ length: 150 }, (_, i) => `# line ${i + 1}`).join("\n"));
53+
view.focus();
54+
view.dispatch({ selection: { anchor: view.state.doc.length }, scrollIntoView: true });
55+
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
56+
const foot = document.getElementById("editor-foot"), column = document.getElementById("editor-column");
57+
const f = foot.getBoundingClientRect();
58+
const caret = view.coordsAtPos(view.state.doc.length);
59+
// the foot's padding strip above its chips: the foot itself, not a chip, is under the pointer there
60+
const hit = document.elementFromPoint(f.left + f.width / 2, f.top + 1);
61+
const out = {
62+
position: getComputedStyle(foot).position, footBottom: f.bottom, columnBottom: column.getBoundingClientRect().bottom,
63+
footTop: f.top, footH: parseFloat(getComputedStyle(column).getPropertyValue("--editor-foot-h")), measured: foot.offsetHeight,
64+
pad: parseFloat(getComputedStyle(view.contentDOM).paddingBottom), caretBottom: caret?.bottom ?? null, hit: hit?.id || hit?.tagName,
65+
};
66+
ed.setCode(before);
67+
return out;
68+
});
69+
check(`${where}: the editor's foot floats over the code's last lines`, r.position === "absolute" && Math.abs(r.footBottom - r.columnBottom) < 1,
70+
`${r.position}, foot ends at ${r.footBottom}, the column at ${r.columnBottom}`);
71+
check(`${where}: the code is padded by the foot's height, as measured`, r.footH === r.measured && r.pad === r.footH,
72+
`--editor-foot-h ${r.footH}, foot ${r.measured}, padding ${r.pad}`);
73+
check(`${where}: the last line, and the caret on it, scroll clear of the foot`, r.caretBottom !== null && r.caretBottom <= r.footTop + 0.5,
74+
`caret ends at ${r.caretBottom}, the foot starts at ${r.footTop}`);
75+
check(`${where}: a tap in the foot's gaps reaches the code`, r.hit === "editor-mount", `under the pointer: ${r.hit}`);
76+
};
4477
// the dev server speaks https with a certificate it signed itself (scripts/serve.mjs): the browser is told to go on
4578
const HTTPS = { ignoreHTTPSErrors: BASE.startsWith("https:") };
4679

@@ -104,6 +137,8 @@ for (const [name, engineType] of [["chromium", chromium], ["webkit", webkit]]) {
104137
};
105138
const caretToEnd = () => page.evaluate(() => { const v = window.sonicPi.editor.view; v.focus(); v.dispatch({ selection: { anchor: v.state.doc.length } }); });
106139

140+
await footFloats(page, "desktop");
141+
107142
// highlighting: native's lexer keys. A cleared buffer has none of them in it, so the code is put there first
108143
await setCode("live_loop :beat do # a comment\n sample :bd_haus, amp: 2\n sleep 1\nend");
109144
await page.waitForSelector("#editor-mount .cm-content .sp-keyword", { timeout: 5000 }); // the buffer's, in its shadow root: not a card's
@@ -990,6 +1025,7 @@ for (const [name, engineType] of [["chromium", chromium], ["webkit", webkit]]) {
9901025
const lit = await ph.locator("#buffer-tabs .buffer-tab.active").getAttribute("data-idx");
9911026
check("on a phone the buffers are pads across the foot, and a tap switches",
9921027
!caretShown && pads === size && activeBuffer === 2 && lit === "2", `caret ${caretShown}, ${pads} pads of ${size}, active ${activeBuffer}, lit ${lit}`);
1028+
await footFloats(ph, "on a phone");
9931029
// the rail's zoom pair (the drawer head, with native's ZoomBar, is hidden on a phone) zooms the pane and leaves it open
9941030
await ph.evaluate(() => document.querySelector("#drawer-rail [data-drawer=docs]").click()); // Help would toggle whatever pane is open
9951031
await ph.waitForFunction(() => document.body.dataset.drawer === "docs");

0 commit comments

Comments
 (0)