Adding support for mouse wheel events. cleaning up click handler to take functions
This commit is contained in:
parent
85451d24fb
commit
1961ac6249
6 changed files with 108 additions and 60 deletions
|
|
@ -12,21 +12,10 @@ center = parentW parentH child \
|
|||
childSize = ui.measure 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 = config \
|
||||
ui.clickable {
|
||||
event = config.event,
|
||||
onClick = config.event,
|
||||
child = ui.stack {
|
||||
children = [
|
||||
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");
|
||||
|
||||
ui.clickable {
|
||||
event = config.onClick,
|
||||
onClick = config.onClick,
|
||||
child = ui.stack {
|
||||
children = [
|
||||
ui.rect { w = config.w, h = config.h, color = color },
|
||||
|
|
@ -109,11 +109,12 @@ palette = config \
|
|||
onChange = text \ batch [paletteState.query := text],
|
||||
},
|
||||
|
||||
scrollable {
|
||||
ui.scrollable {
|
||||
w = contentWidth,
|
||||
h = listHeight,
|
||||
scrollX = 0,
|
||||
scrollY = paletteState.scrollOffset,
|
||||
onScroll = _ \ noOp,
|
||||
child = ui.column {
|
||||
gap = 1,
|
||||
children = [
|
||||
|
|
@ -131,7 +132,7 @@ palette = config \
|
|||
w = contentWidth,
|
||||
h = textInputHeight,
|
||||
selected = (effectiveIndex == i),
|
||||
onClick = config.onSelect data.label
|
||||
onClick = _ \ config.onSelect data.label
|
||||
}
|
||||
| _ \ empty
|
||||
) results)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ renderWindow = window isActive \
|
|||
children = [
|
||||
# close button
|
||||
ui.clickable {
|
||||
event = closeWindowById window.id,
|
||||
onClick = _ \ closeWindowById window.id,
|
||||
child = ui.stack {
|
||||
children = [
|
||||
# button background
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, hitTest } from './ui';
|
||||
import { render, hitTest, scrollHitTest } from './ui';
|
||||
|
||||
type UIValue = any;
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
|
|||
viewUI = {
|
||||
kind: 'clickable',
|
||||
child: viewUI,
|
||||
event: { _tag: 'FocusAndClick', _0: fullKey }
|
||||
onClick: (coords: any) => ({ _tag: 'FocusAndClick', _0: fullKey, _1: coords })
|
||||
};
|
||||
}
|
||||
return expandStateful(viewUI, [...path, ui.key], renderedKeys);
|
||||
|
|
@ -117,6 +117,7 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
|
|||
}
|
||||
|
||||
case 'clickable':
|
||||
case 'scrollable':
|
||||
case 'padding':
|
||||
case 'positioned':
|
||||
case 'opacity':
|
||||
|
|
@ -204,42 +205,7 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
|
|||
return;
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const hitResult = hitTest(x, y);
|
||||
if (hitResult) {
|
||||
const { event, relativeX, relativeY } = hitResult;
|
||||
|
||||
if (event._tag === 'FocusAndClick') {
|
||||
handleEvent({
|
||||
_tag: 'FocusAndClick',
|
||||
_0: event._0,
|
||||
_1: { x: Math.floor(relativeX), y: Math.floor(relativeY) }
|
||||
});
|
||||
} else {
|
||||
handleEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
rerender();
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const event = {
|
||||
_tag: 'Key',
|
||||
_0: {
|
||||
key: e.key,
|
||||
ctrl: { _tag: e.ctrlKey ? 'True' : 'False' },
|
||||
meta: { _tag: e.metaKey ? 'True' : 'False' },
|
||||
alt: { _tag: e.altKey ? 'True' : 'False' },
|
||||
shift: { _tag: e.shiftKey ? 'True' : 'False' },
|
||||
printable: { _tag: (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) ? 'True' : 'False' }
|
||||
}
|
||||
};
|
||||
|
||||
function dispatchToFocused(event: any) {
|
||||
if (focusedComponentKey) {
|
||||
// send to focused component
|
||||
handleComponentEvent(focusedComponentKey, event);
|
||||
|
|
@ -252,6 +218,60 @@ export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const hitResult = hitTest(x, y);
|
||||
if (hitResult) {
|
||||
const coords = { x: Math.floor(hitResult.relativeX), y: Math.floor(hitResult.relativeY) };
|
||||
handleEvent(hitResult.onClick(coords));
|
||||
}
|
||||
|
||||
rerender();
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
dispatchToFocused({
|
||||
_tag: 'Key',
|
||||
_0: {
|
||||
key: e.key,
|
||||
ctrl: { _tag: e.ctrlKey ? 'True' : 'False' },
|
||||
meta: { _tag: e.metaKey ? 'True' : 'False' },
|
||||
alt: { _tag: e.altKey ? 'True' : 'False' },
|
||||
shift: { _tag: e.shiftKey ? 'True' : 'False' },
|
||||
printable: { _tag: (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) ? 'True' : 'False' }
|
||||
}
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
rerender();
|
||||
});
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
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();
|
||||
rerender();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const _rt = {
|
|||
padding: (config: any) => ({ kind: 'padding', ...config }),
|
||||
positioned: (config: any) => ({ kind: 'positioned', ...config }),
|
||||
clickable: (config: any) => ({ kind: 'clickable', ...config }),
|
||||
scrollable: (config: any) => ({ kind: 'scrollable', ...config }),
|
||||
clip: (config: any) => ({ kind: 'clip', ...config }),
|
||||
opacity: (config: any) => ({ kind: 'opacity', ...config }),
|
||||
stateful: (config: any) => ({ kind: 'stateful', ...config }),
|
||||
|
|
|
|||
49
src/ui.ts
49
src/ui.ts
|
|
@ -4,12 +4,13 @@ export type UIValue =
|
|||
| { kind: 'text', content: string, color?: string }
|
||||
| { kind: 'row', children: UIValue[], gap: number }
|
||||
| { kind: 'column', children: UIValue[], gap: number }
|
||||
| { kind: 'clickable', child: UIValue, event: any }
|
||||
| { kind: 'padding', child: UIValue, amount: number }
|
||||
| { kind: 'positioned', x: number, y: number, child: UIValue }
|
||||
| { kind: 'opacity', child: UIValue, opacity: number }
|
||||
| { kind: 'clip', child: UIValue, w: number, h: number }
|
||||
| { 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 }
|
||||
|
||||
type ClickRegion = {
|
||||
|
|
@ -17,11 +18,21 @@ type ClickRegion = {
|
|||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
event: any;
|
||||
onClick: any;
|
||||
};
|
||||
|
||||
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) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
|
|
@ -32,6 +43,7 @@ export function render(ui: UIValue, canvas: HTMLCanvasElement) {
|
|||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
clickRegions = [];
|
||||
scrollRegions = [];
|
||||
renderUI(ui, ctx, 0, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +100,7 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
|
|||
case 'row': {
|
||||
let offsetX = 0;
|
||||
for (const child of ui.children) {
|
||||
const size = measure(child);
|
||||
// const size = measure(child);
|
||||
renderUI(child, ctx, x + offsetX, y);
|
||||
offsetX += measure(child).width + (ui.gap || 0);
|
||||
}
|
||||
|
|
@ -106,11 +118,22 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
|
|||
|
||||
case 'clickable': {
|
||||
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);
|
||||
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':
|
||||
renderUI(ui.child, ctx, x + ui.amount, y + ui.amount);
|
||||
break;
|
||||
|
|
@ -187,6 +210,9 @@ export function _measure(ui: UIValue): { width: number, height: number } {
|
|||
case 'clickable':
|
||||
return measure(ui.child);
|
||||
|
||||
case 'scrollable':
|
||||
return { width: ui.w, height: ui.h };
|
||||
|
||||
case 'opacity':
|
||||
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--) {
|
||||
const region = clickRegions[i];
|
||||
|
||||
if (x >= region.x && x < region.x + region.width &&
|
||||
y >= region.y && y < region.y + region.height) {
|
||||
return {
|
||||
event: region.event,
|
||||
onClick: region.onClick,
|
||||
relativeX: x - region.x,
|
||||
relativeY: y - region.y,
|
||||
};
|
||||
|
|
@ -238,3 +264,14 @@ export function hitTest(x: number, y: number): { event: any, relativeX: number,
|
|||
}
|
||||
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…
Add table
Add a link
Reference in a new issue