creating a CG text input
This commit is contained in:
parent
12d27a1bff
commit
bc186d658c
6 changed files with 128 additions and 44 deletions
|
|
@ -1,5 +1,11 @@
|
||||||
import type { Value, NativeFunction } from './types'
|
import type { Value, NativeFunction } from './types'
|
||||||
|
|
||||||
|
const measureCanvas = document.createElement('canvas');
|
||||||
|
const measureCtx = measureCanvas.getContext('2d')!;
|
||||||
|
if (!measureCtx)
|
||||||
|
throw new Error('Failed to create canvas');
|
||||||
|
measureCtx.font = '16px "Courier New", monospace';
|
||||||
|
|
||||||
function expectInt(v: Value, name: string): number {
|
function expectInt(v: Value, name: string): number {
|
||||||
if (v.kind !== 'int')
|
if (v.kind !== 'int')
|
||||||
throw new Error(`${name} expects int, got ${v.kind}`);
|
throw new Error(`${name} expects int, got ${v.kind}`);
|
||||||
|
|
@ -18,11 +24,11 @@ function expectNumber(v: Value, name: string): number {
|
||||||
return v.value;
|
return v.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// function expectString(v: Value, name: string): string {
|
function expectString(v: Value, name: string): string {
|
||||||
// if (v.kind !== 'string')
|
if (v.kind !== 'string')
|
||||||
// throw new Error(`${name} expects string, got ${v.kind}`);
|
throw new Error(`${name} expects string, got ${v.kind}`);
|
||||||
// return v.value;
|
return v.value;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// function expectList(v: Value, name: string): Value[] {
|
// function expectList(v: Value, name: string): Value[] {
|
||||||
// if (v.kind !== 'list')
|
// if (v.kind !== 'list')
|
||||||
|
|
@ -345,7 +351,7 @@ export const builtins: { [name: string]: NativeFunction } = {
|
||||||
return { kind: 'float', value: val.value };
|
return { kind: 'float', value: val.value };
|
||||||
|
|
||||||
if (val.kind === 'string') {
|
if (val.kind === 'string') {
|
||||||
const parsed = parseFloat(val.value, 10);
|
const parsed = parseFloat(val.value);
|
||||||
|
|
||||||
if (isNaN(parsed))
|
if (isNaN(parsed))
|
||||||
throw new Error(`float: cannot parse "${val.value}"`);
|
throw new Error(`float: cannot parse "${val.value}"`);
|
||||||
|
|
@ -444,4 +450,16 @@ export const builtins: { [name: string]: NativeFunction } = {
|
||||||
return { kind: 'int', value: result };
|
return { kind: 'int', value: result };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'measureText': {
|
||||||
|
kind: 'native',
|
||||||
|
name: 'measureText',
|
||||||
|
arity: 1,
|
||||||
|
fn: (text) => {
|
||||||
|
const str = expectString(text, 'measureText');
|
||||||
|
// TODO
|
||||||
|
const metrics = measureCtx.measureText(str);
|
||||||
|
return { kind: 'float', value: metrics.width };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Value } from './types';
|
import type { Value } from './types';
|
||||||
import { valueToUI } from './valueToUI';
|
import { valueToUI } from './valueToUI';
|
||||||
import { render, hitTest, hitTestTextInput, handleKeyboard } from './ui';
|
import { render, hitTest, hitTestTextInput } from './ui';
|
||||||
import { evaluate } from './interpreter';
|
import { evaluate } from './interpreter';
|
||||||
|
|
||||||
export type App = {
|
export type App = {
|
||||||
|
|
@ -81,15 +81,27 @@ export function runApp(app: App, canvas: HTMLCanvasElement) {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
const event = handleKeyboard(e.key);
|
let event: Value | null = null;
|
||||||
|
|
||||||
if (event) {
|
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
handleEvent(event);
|
event = {
|
||||||
e.preventDefault();
|
kind: 'constructor',
|
||||||
|
name: 'Char',
|
||||||
|
args: [{ kind: 'string', value: e.key }]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event = {
|
||||||
|
kind: 'constructor',
|
||||||
|
name: e.key,
|
||||||
|
args: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('resize', (e) => {
|
handleEvent(event);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
setupCanvas();
|
setupCanvas();
|
||||||
rerender();
|
rerender();
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,78 @@
|
||||||
init = { text = "" };
|
# Helpers
|
||||||
|
|
||||||
|
insertChar = text pos char \
|
||||||
|
before = slice text 0 pos;
|
||||||
|
after = slice text pos (len text);
|
||||||
|
before & char & after;
|
||||||
|
|
||||||
|
deleteChar = text pos \
|
||||||
|
(pos == 0
|
||||||
|
| True \ text
|
||||||
|
| False \
|
||||||
|
(before = slice text 0 (pos - 1);
|
||||||
|
after = slice text pos (len text);
|
||||||
|
before & after));
|
||||||
|
|
||||||
|
# test app
|
||||||
|
|
||||||
|
init = {
|
||||||
|
text = "hello world",
|
||||||
|
cursorPos = 5
|
||||||
|
};
|
||||||
|
|
||||||
update = state event \ event
|
update = state event \ event
|
||||||
| UpdateText newText \ state.{ text = newText }
|
| ArrowLeft \ state.{ cursorPos = max 0 (state.cursorPos - 1) }
|
||||||
| Submit _ \ state.{ text = "" }
|
| ArrowRight \ state.{ cursorPos = min (len state.text) (state.cursorPos + 1) }
|
||||||
| Go \ state.{ text = "" };
|
| Backspace \ {
|
||||||
|
text = deleteChar state.text state.cursorPos,
|
||||||
|
cursorPos = max 0 (state.cursorPos - 1)
|
||||||
|
}
|
||||||
|
| Char c \ {
|
||||||
|
text = insertChar state.text state.cursorPos c,
|
||||||
|
cursorPos = state.cursorPos + 1
|
||||||
|
}
|
||||||
|
| _ \ state;
|
||||||
|
|
||||||
view = state viewport \
|
view = state viewport \
|
||||||
Padding {
|
# charWidth = 9.65;
|
||||||
amount = 20,
|
# cursorX = state.cursorPos * charWidth;
|
||||||
child = Column {
|
textBeforeCursor = slice state.text 0 state.cursorPos;
|
||||||
gap = 20,
|
cursorX = measureText textBeforeCursor;
|
||||||
|
|
||||||
|
Column {
|
||||||
|
gap = 20,
|
||||||
|
children = [
|
||||||
|
Text {
|
||||||
|
content = "Text: " & state.text & " | Cursor: " & str(state.cursorPos),
|
||||||
|
x = 0,
|
||||||
|
y = 20
|
||||||
|
},
|
||||||
|
|
||||||
|
# Text Input Component
|
||||||
|
Stack {
|
||||||
children = [
|
children = [
|
||||||
Text {
|
Rect { w = 300, h = 40, color = "white", radius = 4 },
|
||||||
content = "window: " & str(viewport.width) & " x " & str(viewport.height),
|
|
||||||
x = 0,
|
# Text content
|
||||||
y = 20
|
Positioned {
|
||||||
|
x = 8,
|
||||||
|
y = 8,
|
||||||
|
child = Text { content = state.text, x = 0, y = 17 }
|
||||||
},
|
},
|
||||||
Text { content = "You typed: " & state.text, x = 0, y = 20 },
|
|
||||||
Stack {
|
# Cursor
|
||||||
children = [
|
Positioned {
|
||||||
Rect { w = 300, h = 40, color = "blue", radius = 2 },
|
x = 8 + cursorX,
|
||||||
TextInput {
|
y = 8,
|
||||||
value = state.text,
|
child = Rect {
|
||||||
placeholder = "Type something...",
|
w = 2,
|
||||||
x = 5,
|
h = 24,
|
||||||
y = 5,
|
color = "black"
|
||||||
w = 290,
|
}
|
||||||
h = 30,
|
}
|
||||||
focused = True,
|
|
||||||
onInput = UpdateText,
|
|
||||||
onSubmit = Submit
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
button { label = "Go", event = Go, theme = theme }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
{ init = init, update = update, view = view }
|
{ init = init, update = update, view = view }
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export type UIValue =
|
||||||
| { kind: 'column', children: UIValue[], gap: number }
|
| { kind: 'column', children: UIValue[], gap: number }
|
||||||
| { kind: 'clickable', child: UIValue, event: Value }
|
| { kind: 'clickable', child: UIValue, event: Value }
|
||||||
| { kind: 'padding', child: UIValue, amount: number }
|
| { kind: 'padding', child: UIValue, amount: number }
|
||||||
|
| { kind: 'positioned', x: number, y: number, child: UIValue }
|
||||||
| { kind: 'opacity', child: UIValue, opacity: number }
|
| { kind: 'opacity', child: UIValue, opacity: number }
|
||||||
| { kind: 'stack', children: UIValue[] }
|
| { kind: 'stack', children: UIValue[] }
|
||||||
| { kind: 'text-input', value: string, placeholder: string, x: number, y: number, w: number, h: number, focused: boolean, onInput: Value, onSubmit: Value }
|
| { kind: 'text-input', value: string, placeholder: string, x: number, y: number, w: number, h: number, focused: boolean, onInput: Value, onSubmit: Value }
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = 'black';
|
||||||
ctx.font = '16px monospace';
|
ctx.font = '16px "Courier New", monospace';
|
||||||
ctx.fillText(ui.content, x + ui.x, y + ui.y);
|
ctx.fillText(ui.content, x + ui.x, y + ui.y);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -98,6 +98,10 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb
|
||||||
renderUI(ui.child, ctx, x + ui.amount, y + ui.amount);
|
renderUI(ui.child, ctx, x + ui.amount, y + ui.amount);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'positioned':
|
||||||
|
renderUI(ui.child, ctx, x + ui.x, y + ui.y);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'opacity': {
|
case 'opacity': {
|
||||||
const previousAlpha = ctx.globalAlpha;
|
const previousAlpha = ctx.globalAlpha;
|
||||||
ctx.globalAlpha = previousAlpha * ui.opacity;
|
ctx.globalAlpha = previousAlpha * ui.opacity;
|
||||||
|
|
@ -188,6 +192,9 @@ function measure(ui: UIValue): { width: number, height: number } {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'positioned':
|
||||||
|
return measure(ui.child);
|
||||||
|
|
||||||
case 'stack': {
|
case 'stack': {
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,15 @@ export function valueToUI(value: Value): UIValue {
|
||||||
return { kind: 'padding', amount: amount.value, child: valueToUI(child) };
|
return { kind: 'padding', amount: amount.value, child: valueToUI(child) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'Positioned': {
|
||||||
|
const { x, y, child } = fields;
|
||||||
|
|
||||||
|
if (x.kind !== 'int' || y.kind !== 'int')
|
||||||
|
throw new Error('Invalid Positioned fields');
|
||||||
|
|
||||||
|
return { kind: 'positioned', x: x.value, y: y.value, child: valueToUI(child) };
|
||||||
|
}
|
||||||
|
|
||||||
case 'Opacity': {
|
case 'Opacity': {
|
||||||
const { child, opacity } = fields;
|
const { child, opacity } = fields;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue