Adding support for mouse wheel events. cleaning up click handler to take functions

master
Dustin Swan 2 weeks ago
parent 85451d24fb
commit 1961ac6249
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -12,21 +12,10 @@ center = parentW parentH child \
childSize = ui.measure child; childSize = ui.measure child;
ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child };
scrollable = config \
ui.clip {
w = config.w,
h = config.h,
child = ui.positioned {
x = 0 - config.scrollX,
y = 0 - config.scrollY,
child = config.child
}
};
# button : Record -> ui # button : Record -> ui
button = config \ button = config \
ui.clickable { ui.clickable {
event = config.event, onClick = config.event,
child = ui.stack { child = ui.stack {
children = [ children = [
ui.rect { w = 100, h = 40, color = "#eee" }, ui.rect { w = 100, h = 40, color = "#eee" },

@ -50,7 +50,7 @@ palette = config \
color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent"); color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent");
ui.clickable { ui.clickable {
event = config.onClick, onClick = config.onClick,
child = ui.stack { child = ui.stack {
children = [ children = [
ui.rect { w = config.w, h = config.h, color = color }, ui.rect { w = config.w, h = config.h, color = color },
@ -109,11 +109,12 @@ palette = config \
onChange = text \ batch [paletteState.query := text], onChange = text \ batch [paletteState.query := text],
}, },
scrollable { ui.scrollable {
w = contentWidth, w = contentWidth,
h = listHeight, h = listHeight,
scrollX = 0, scrollX = 0,
scrollY = paletteState.scrollOffset, scrollY = paletteState.scrollOffset,
onScroll = _ \ noOp,
child = ui.column { child = ui.column {
gap = 1, gap = 1,
children = [ children = [
@ -131,7 +132,7 @@ palette = config \
w = contentWidth, w = contentWidth,
h = textInputHeight, h = textInputHeight,
selected = (effectiveIndex == i), selected = (effectiveIndex == i),
onClick = config.onSelect data.label onClick = _ \ config.onSelect data.label
} }
| _ \ empty | _ \ empty
) results) ) results)

@ -100,7 +100,7 @@ renderWindow = window isActive \
children = [ children = [
# close button # close button
ui.clickable { ui.clickable {
event = closeWindowById window.id, onClick = _ \ closeWindowById window.id,
child = ui.stack { child = ui.stack {
children = [ children = [
# button background # button background

@ -1,4 +1,4 @@
import { render, hitTest } from './ui'; import { render, hitTest, scrollHitTest } from './ui';
type UIValue = any; type UIValue = any;
@ -99,7 +99,7 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
viewUI = { viewUI = {
kind: 'clickable', kind: 'clickable',
child: viewUI, child: viewUI,
event: { _tag: 'FocusAndClick', _0: fullKey } onClick: (coords: any) => ({ _tag: 'FocusAndClick', _0: fullKey, _1: coords })
}; };
} }
return expandStateful(viewUI, [...path, ui.key], renderedKeys); return expandStateful(viewUI, [...path, ui.key], renderedKeys);
@ -117,6 +117,7 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
} }
case 'clickable': case 'clickable':
case 'scrollable':
case 'padding': case 'padding':
case 'positioned': case 'positioned':
case 'opacity': case 'opacity':
@ -204,6 +205,21 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
return; return;
} }
function dispatchToFocused(event: any) {
if (focusedComponentKey) {
// send to focused component
handleComponentEvent(focusedComponentKey, event);
// bubble up to ancestors
for (const key of componentInstances.keys()) {
if (key !== focusedComponentKey && focusedComponentKey.startsWith(key + '.')) {
handleComponentEvent(key, event);
}
}
}
}
canvas.addEventListener('click', (e) => { canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
@ -211,24 +227,15 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
const hitResult = hitTest(x, y); const hitResult = hitTest(x, y);
if (hitResult) { if (hitResult) {
const { event, relativeX, relativeY } = hitResult; const coords = { x: Math.floor(hitResult.relativeX), y: Math.floor(hitResult.relativeY) };
handleEvent(hitResult.onClick(coords));
if (event._tag === 'FocusAndClick') {
handleEvent({
_tag: 'FocusAndClick',
_0: event._0,
_1: { x: Math.floor(relativeX), y: Math.floor(relativeY) }
});
} else {
handleEvent(event);
}
} }
rerender(); rerender();
}); });
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
const event = { dispatchToFocused({
_tag: 'Key', _tag: 'Key',
_0: { _0: {
key: e.key, key: e.key,
@ -238,20 +245,33 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
shift: { _tag: e.shiftKey ? 'True' : 'False' }, shift: { _tag: e.shiftKey ? 'True' : 'False' },
printable: { _tag: (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) ? 'True' : 'False' } printable: { _tag: (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) ? 'True' : 'False' }
} }
}; });
if (focusedComponentKey) { e.preventDefault();
// send to focused component rerender();
handleComponentEvent(focusedComponentKey, event); });
// bubble up to ancestors canvas.addEventListener('wheel', (e) => {
for (const key of componentInstances.keys()) { const rect = canvas.getBoundingClientRect();
if (key !== focusedComponentKey && focusedComponentKey.startsWith(key + '.')) { const x = e.clientX - rect.left;
handleComponentEvent(key, event); const y = e.clientY - rect.top;
}
} const hit = scrollHitTest(x, y);
if (hit) {
const delta = { deltaX: Math.round(e.deltaX), deltaY: Math.round(e.deltaY) };
handleEvent(hit.onScroll(delta));
} }
/*
dispatchToFocused({
_tag: 'Scroll',
_0: {
deltaX: Math.round(e.deltaX),
deltaY: Math.round(e.deltaY)
}
});
*/
e.preventDefault(); e.preventDefault();
rerender(); rerender();
}); });

@ -36,6 +36,7 @@ export const _rt = {
padding: (config: any) => ({ kind: 'padding', ...config }), padding: (config: any) => ({ kind: 'padding', ...config }),
positioned: (config: any) => ({ kind: 'positioned', ...config }), positioned: (config: any) => ({ kind: 'positioned', ...config }),
clickable: (config: any) => ({ kind: 'clickable', ...config }), clickable: (config: any) => ({ kind: 'clickable', ...config }),
scrollable: (config: any) => ({ kind: 'scrollable', ...config }),
clip: (config: any) => ({ kind: 'clip', ...config }), clip: (config: any) => ({ kind: 'clip', ...config }),
opacity: (config: any) => ({ kind: 'opacity', ...config }), opacity: (config: any) => ({ kind: 'opacity', ...config }),
stateful: (config: any) => ({ kind: 'stateful', ...config }), stateful: (config: any) => ({ kind: 'stateful', ...config }),

@ -4,12 +4,13 @@ export type UIValue =
| { kind: 'text', content: string, color?: string } | { kind: 'text', content: string, color?: string }
| { kind: 'row', children: UIValue[], gap: number } | { kind: 'row', children: UIValue[], gap: number }
| { kind: 'column', children: UIValue[], gap: number } | { kind: 'column', children: UIValue[], gap: number }
| { kind: 'clickable', child: UIValue, event: any }
| { kind: 'padding', child: UIValue, amount: number } | { kind: 'padding', child: UIValue, amount: number }
| { kind: 'positioned', x: number, y: number, child: UIValue } | { kind: 'positioned', x: number, y: number, child: UIValue }
| { kind: 'opacity', child: UIValue, opacity: number } | { kind: 'opacity', child: UIValue, opacity: number }
| { kind: 'clip', child: UIValue, w: number, h: number } | { kind: 'clip', child: UIValue, w: number, h: number }
| { kind: 'stack', children: UIValue[] } | { kind: 'stack', children: UIValue[] }
| { kind: 'clickable', child: UIValue, onClick: any }
| { kind: 'scrollable', child: UIValue, w: number, h: number, scrollX: number, scrollY: number, onScroll: any }
| { kind: 'stateful', key: string, focusable: boolean, init: any, update: any, view: any } | { kind: 'stateful', key: string, focusable: boolean, init: any, update: any, view: any }
type ClickRegion = { type ClickRegion = {
@ -17,11 +18,21 @@ type ClickRegion = {
y: number; y: number;
width: number; width: number;
height: number; height: number;
event: any; onClick: any;
}; };
let clickRegions: ClickRegion[] = []; let clickRegions: ClickRegion[] = [];
type ScrollRegion = {
x: number;
y: number;
width: number;
height: number;
onScroll: any;
};
let scrollRegions: ScrollRegion[] = [];
export function render(ui: UIValue, canvas: HTMLCanvasElement) { export function render(ui: UIValue, canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
@ -32,6 +43,7 @@ export function render(ui: UIValue, canvas: HTMLCanvasElement) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
clickRegions = []; clickRegions = [];
scrollRegions = [];
renderUI(ui, ctx, 0, 0); renderUI(ui, ctx, 0, 0);
} }
} }
@ -88,7 +100,7 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
case 'row': { case 'row': {
let offsetX = 0; let offsetX = 0;
for (const child of ui.children) { for (const child of ui.children) {
const size = measure(child); // const size = measure(child);
renderUI(child, ctx, x + offsetX, y); renderUI(child, ctx, x + offsetX, y);
offsetX += measure(child).width + (ui.gap || 0); offsetX += measure(child).width + (ui.gap || 0);
} }
@ -106,11 +118,22 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
case 'clickable': { case 'clickable': {
const size = measure(ui.child); const size = measure(ui.child);
clickRegions.push({ x, y, width: size.width, height: size.height, event: ui.event }) clickRegions.push({ x, y, width: size.width, height: size.height, onClick: ui.onClick })
renderUI(ui.child, ctx, x, y); renderUI(ui.child, ctx, x, y);
break; break;
} }
case 'scrollable': {
ctx.save();
ctx.beginPath();
ctx.rect(x, y, ui.w, ui.h);
ctx.clip();
renderUI(ui.child, ctx, x - ui.scrollX, y - ui.scrollY);
ctx.restore();
scrollRegions.push({ x, y, width: ui.w, height: ui.h, onScroll: ui.onScroll })
break;
}
case 'padding': case 'padding':
renderUI(ui.child, ctx, x + ui.amount, y + ui.amount); renderUI(ui.child, ctx, x + ui.amount, y + ui.amount);
break; break;
@ -187,6 +210,9 @@ export function _measure(ui: UIValue): { width: number, height: number } {
case 'clickable': case 'clickable':
return measure(ui.child); return measure(ui.child);
case 'scrollable':
return { width: ui.w, height: ui.h };
case 'opacity': case 'opacity':
return measure(ui.child); return measure(ui.child);
@ -223,14 +249,14 @@ export function _measure(ui: UIValue): { width: number, height: number } {
} }
} }
export function hitTest(x: number, y: number): { event: any, relativeX: number, relativeY: number } | null { export function hitTest(x: number, y: number): { onClick: any, relativeX: number, relativeY: number } | null {
for (let i = clickRegions.length - 1; i >= 0; i--) { for (let i = clickRegions.length - 1; i >= 0; i--) {
const region = clickRegions[i]; const region = clickRegions[i];
if (x >= region.x && x < region.x + region.width && if (x >= region.x && x < region.x + region.width &&
y >= region.y && y < region.y + region.height) { y >= region.y && y < region.y + region.height) {
return { return {
event: region.event, onClick: region.onClick,
relativeX: x - region.x, relativeX: x - region.x,
relativeY: y - region.y, relativeY: y - region.y,
}; };
@ -238,3 +264,14 @@ export function hitTest(x: number, y: number): { event: any, relativeX: number,
} }
return null; return null;
} }
export function scrollHitTest(x: number, y: number): { onScroll: any } | null {
for (let i = scrollRegions.length - 1; i >= 0; i--) {
const region = scrollRegions[i];
if (x >= region.x && x < region.x + region.width &&
y >= region.y && y < region.y + region.height) {
return { onScroll: region.onScroll };
}
}
return null;
}

Loading…
Cancel
Save