Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions crates/embedder-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
.control:focus-visible { outline: 2px solid #24211c; outline-offset: -4px; }
.semantic-group { position: absolute; pointer-events: none; }
.panel:focus-visible { outline: 2px solid #24211c; outline-offset: -4px; }
.control.edit { font: 14px/20px monospace; padding: 8px; color: #24211c; background: #f5efdf;
border: 1px solid #24211c; resize: none; cursor: text; border-radius: 0; }
.control.edit { font: 14px/20px monospace; padding: 8px; color: transparent; caret-color: #24211c;
background: transparent; border: 1px solid transparent; resize: none; cursor: text; border-radius: 0;
overflow: auto; overscroll-behavior: contain; white-space: pre; }
.control.edit::selection { background: #f4d35e; color: transparent; }
.control.edit.ime { color: #24211c; }
@media (pointer: coarse) { .control.edit { font-size: 16px; } }
#feedback { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
#status { position: absolute; top: 1rem; left: 1rem; font: 16px sans-serif; }
Expand Down Expand Up @@ -79,10 +82,12 @@
if (editing) {
node.classList.add('edit');
node.spellcheck = false;
node.addEventListener('compositionstart', () => { node.composing = true; });
if (item.role === 41) node.wrap = 'off';
node.addEventListener('compositionstart', () => { node.composing = true; node.classList.add('ime'); });
node.addEventListener('compositionupdate', event => Module.syncEdit(node, 1, event.data));
node.addEventListener('compositionend', () => {
node.composing = false;
node.classList.remove('ime');
Module.syncEdit(node);
});
node.addEventListener('input', event => {
Expand Down Expand Up @@ -242,13 +247,25 @@
if (!getSelection().isCollapsed) return;
if (Module.ccall('sz_web_key', 'number', ['string', 'number', 'number'], [event.key, event.shiftKey ? 1 : 0, Number(event.repeat)])) event.preventDefault();
});
let touchX;
let touchY;
layer.addEventListener('touchstart', event => { touchY = event.touches[0].clientY; }, {passive: true});
const panEdit = (node, dx, dy) => {
if (!node) return;
node.scrollLeft += dx;
node.scrollTop += dy;
};
layer.addEventListener('touchstart', event => {
touchX = event.touches[0].clientX; touchY = event.touches[0].clientY;
}, {passive: true});
layer.addEventListener('touchmove', event => {
if (!getSelection().isCollapsed || event.touches.length !== 1 || event.target.closest('.edit')) return;
if (!getSelection().isCollapsed || event.touches.length !== 1) return;
const touch = event.touches[0];
Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'], [touch.clientX, touch.clientY, 0, touchY - touch.clientY]);
touchY = touch.clientY; event.preventDefault();
const dx = touchX - touch.clientX;
const dy = touchY - touch.clientY;
panEdit(event.target.closest('.edit'), dx, dy);
Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'],
[touch.clientX, touch.clientY, dx, dy]);
touchX = touch.clientX; touchY = touch.clientY; event.preventDefault();
}, {passive: false});
// Wheel and canvas touch cancel the event. Register those listeners as non-passive.
window.addEventListener('wheel', event => {
Expand All @@ -257,6 +274,7 @@
let dx = event.deltaX * scale;
let dy = event.deltaY * scale;
if (event.shiftKey && dx === 0) { dx = dy; dy = 0; }
panEdit(event.target.closest?.('.edit'), dx, dy);
Module.ccall('sz_web_scroll', null, ['number', 'number', 'number', 'number'],
[event.clientX, event.clientY, dx, dy]);
event.preventDefault();
Expand Down
123 changes: 96 additions & 27 deletions crates/embedder-web/test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ async function check(browserType, url, mobile) {
}, await locator.elementHandle());
};
await page.goto(url);
await expectText('text:Run inc');
await expectSection('run');
await expectText('text:Scuzz Lang');
await expectSection('intro');
await expectText('text:Intro 1/8');
assert.equal(await page.title(), 'Scuzz');
{
const paints = await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], []));
Expand All @@ -82,18 +83,70 @@ async function check(browserType, url, mobile) {
assert.equal(await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], [])), pumps);
assert.equal(await page.evaluate(() => window.rafRequests), frames, 'idle frame loop');
}
assert.equal(await page.getByRole('heading', {name: 'Run', level: 1}).count(), 1);
assert.equal(await page.getByRole('heading', {name: 'Intro 1/8', level: 1}).count(), 1);
assert.equal(await page.getByRole('navigation', {name: 'Breadcrumb'}).count(), 0);
assert.equal(await page.getByRole('region', {name: 'App bar'}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'Intro', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'Run', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'View', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'State', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'Cover', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'Mutation', exact: true}).count(), 1);
assert.equal(await page.getByRole('button', {name: 'Add one', exact: true}).count(), 0);
assert.equal(await page.getByRole('img').count(), 0);
const introContinue = page.getByRole('button', {name: 'Continue', exact: true});
await reveal(introContinue);
await introContinue.click();
await expectSection('run');
await expectText('text:Run inc');
const run = page.getByRole('button', {name: 'Run', exact: true});
await reveal(run);
await run.click();
await expectText('text:1');
{
const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true});
const original = await tryEditor.inputValue();
const overlay = await tryEditor.evaluate(node => {
const computed = getComputedStyle(node);
return {color: computed.color, background: computed.backgroundColor};
});
assert.equal(overlay.color, 'rgba(0, 0, 0, 0)', JSON.stringify(overlay));
assert.equal(overlay.background, 'rgba(0, 0, 0, 0)', JSON.stringify(overlay));
await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.insertText(Array.from({length: 40}, (_, i) => `line ${i}`).join('\n'));
const scrolled = await tryEditor.evaluate(node => {
const rect = node.getBoundingClientRect();
const before = node.scrollTop;
const wheel = new WheelEvent('wheel', {bubbles: true, cancelable: true, deltaY: 120,
clientX: rect.left + 24, clientY: rect.top + 24});
node.dispatchEvent(wheel);
return {before, after: node.scrollTop, height: node.scrollHeight, client: node.clientHeight};
});
assert(scrolled.height > scrolled.client, JSON.stringify(scrolled));
assert(scrolled.after > scrolled.before, JSON.stringify(scrolled));
if (mobile && browserType === chromium) {
const swiped = await tryEditor.evaluate(node => {
node.scrollTop = 0;
const rect = node.getBoundingClientRect();
const x = rect.left + 24;
const y0 = rect.top + 80;
const y1 = rect.top + 20;
const before = node.scrollTop;
const startTouch = new Touch({identifier: 1, target: node, clientX: x, clientY: y0});
const moveTouch = new Touch({identifier: 1, target: node, clientX: x, clientY: y1});
node.dispatchEvent(new TouchEvent('touchstart', {bubbles: true, cancelable: true,
touches: [startTouch], changedTouches: [startTouch]}));
const move = new TouchEvent('touchmove', {bubbles: true, cancelable: true,
touches: [moveTouch], changedTouches: [moveTouch]});
node.dispatchEvent(move);
return {before, after: node.scrollTop, prevented: move.defaultPrevented};
});
assert(swiped.after > swiped.before, JSON.stringify(swiped));
assert.equal(swiped.prevented, true);
}
await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.insertText(original);
}
assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1);
const continueRun = page.getByRole('button', {name: 'Continue', exact: true});
await reveal(continueRun);
Expand All @@ -110,24 +163,29 @@ async function check(browserType, url, mobile) {
await expectText('text:Clicks: 0');
await page.getByRole('tab', {name: 'Check', exact: true}).click();
await expectSection('check');
await expectText('text:Check 4/8');
assert.equal(await page.getByRole('tab', {name: 'Main.scuzz', exact: true}).count(), 1);
assert.equal(await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).count(), 1);
await page.getByRole('tab', {name: 'count.scuzz_verify', exact: true}).click();
await expectEditor('oracle incAdds');
const check = page.getByRole('button', {name: 'Check', exact: true});
await reveal(check);
await check.click();
await expectText('text:true');
const signalTab = page.getByRole('tab', {name: 'Signal', exact: true});
await reveal(signalTab);
await signalTab.click();
await expectSection('signal');
assert.equal(new URL(page.url()).hash, '#stage=signal');
const addOne = page.getByRole('button', {name: 'Add one', exact: true});
await addOne.waitFor({state: 'attached'});
await reveal(addOne);
await addOne.click();
await expectText('text:Count: 1');
const stateTab = page.getByRole('tab', {name: 'State', exact: true});
await reveal(stateTab);
await stateTab.click();
await expectSection('state');
assert.equal(new URL(page.url()).hash, '#stage=state');
await expectText('text:State 5/8');
const stateRun = page.getByRole('button', {name: 'Run', exact: true});
await reveal(stateRun);
await stateRun.click();
await expectText('text:Clicks: 0');
const plusState = page.getByRole('button', {name: '+1', exact: true});
await reveal(plusState);
await plusState.click();
await expectText('text:Clicks: 1');
assert.equal(await page.getByRole('button', {name: 'Continue', exact: true}).count(), 1);
{
const paints = await page.evaluate(() => Module.ccall('sz_web_paints', 'number', [], []));
const pumps = await page.evaluate(() => Module.ccall('sz_web_pumps', 'number', [], []));
Expand All @@ -146,28 +204,27 @@ async function check(browserType, url, mobile) {
await fuzz.click();
await expectText('text:fail hidden 3');
await expectSnap('chip:fail=1');
await page.getByRole('tab', {name: 'Signal', exact: true}).click();
await expectText('text:Count: 1');
const signalRun = page.getByRole('button', {name: 'Run', exact: true});
await reveal(signalRun);
await signalRun.click();
await page.getByRole('tab', {name: 'State', exact: true}).click();
await expectText('text:Clicks: 1');
await reveal(stateRun);
await stateRun.click();
await expectText('text:Clicks: 0');
const tryEditor = page.getByRole('textbox', {name: 'editor', exact: true});
const trySource = await tryEditor.inputValue();
assert(trySource.includes('Clicks: $n'));
await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.insertText(trySource.replace('Clicks: $n', 'Taps: $n'));
await reveal(signalRun);
await signalRun.click();
await reveal(stateRun);
await stateRun.click();
await expectText('text:Taps: 0');
const plusMounted = page.getByRole('button', {name: '+1', exact: true});
await reveal(plusMounted);
await plusMounted.click();
await expectText('text:Taps: 1');
await tryEditor.focus(); await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.insertText('@main def main: IO[Unit] = Ui.run(_ => View.text(1))');
await reveal(signalRun);
await signalRun.click();
await reveal(stateRun);
await stateRun.click();
await page.waitForFunction(() => Module.textBlocks?.some(block => /expected String/.test(block.text)));
await page.getByRole('tab', {name: 'Cover', exact: true}).click();
await expectSection('cover');
Expand All @@ -180,11 +237,23 @@ async function check(browserType, url, mobile) {
console.error({cover: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))});
throw error;
});
await expectSnap('text:arms ');
await expectSnap('text:live ');
await expectSnap('text:mutant ');
await expectSnap('text:hits ');
await expectSnap('semantics:cover-hit');
await page.getByRole('tab', {name: 'Mutation', exact: true}).click();
await expectSection('mutation');
await expectSnap('text:live source');
await expectSnap('text:mutant source');
await expectSnap('text:- ');
await expectSnap('text:+ ');
await page.waitForFunction(() => Module.ccall('sz_web_snapshot', 'string', [], []).includes('text:incAdds reject'),
null, {timeout: 60000}).catch(async error => {
console.error({mutation: await page.evaluate(() => Module.ccall('sz_web_snapshot', 'string', [], []))});
throw error;
});
assert.equal(await page.getByRole('link', {name: 'Scuzz on GitHub', exact: true}).getAttribute('href'),
'https://github.com/SeanCheatham/scuzz');
await page.getByRole('tab', {name: 'Cover', exact: true}).click();
await expectSection('cover');
await page.getByRole('button', {name: 'Copy', exact: true}).first().click();
await page.getByRole('button', {name: 'Copied', exact: true}).first().waitFor();
if (browserType === chromium) {
Expand Down Expand Up @@ -235,7 +304,7 @@ async function check(browserType, url, mobile) {
await runTab.focus();
await page.keyboard.press('End');
await page.keyboard.press('Enter');
await expectSection('cover');
await expectSection('mutation');
await page.goto(url + '?preview=1#stage=search');
await expectSection('search');
await page.reload(); await expectSection('search');
Expand Down
2 changes: 1 addition & 1 deletion crates/runtime/src/view.c
Original file line number Diff line number Diff line change
Expand Up @@ -3142,7 +3142,7 @@ static void layout_node_ex(SzView *v, float x, float y, float min_w, float min_h
case SZ_VIEW_EDITOR: {
float font_px = theme->font_px;
float line_h = text_line_h(theme, font_px);
float h = 8.f * line_h;
float h = 16.f * line_h;
v->frame.w = max_w > 0 ? max_w : 120.f;
if (max_h > 0.f && h > max_h)
h = max_h;
Expand Down
Loading