class UIElement { constructor(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; this.hovered = false; this.active = false; } contains(px, py) { return px >= this.x && px <= this.x + this.w && py >= this.y && py <= this.y + this.h; } handlePointerEvent(event, px, py) { switch (event.type) { case 'pointermove': this.onPointerMove(px, py); break; case 'pointerdown': this.onPointerDown(px, py); break; case 'pointerup': this.onPointerUp(px, py); break; case 'click': // optional: treat click as pointerup for convenience if (this.contains(px, py)) { this.onClick?.(); // if subclass defines onClick } break; } } onPointerMove(px, py) { this.hovered = this.contains(px, py); //console.log(this); } onPointerDown(px, py) { if (this.contains(px, py)) { this.active = true; return true; } return false; } onPointerUp(px, py) { if (this.active && this.contains(px, py)) { this.active = false; return true; } this.active = false; return false; } draw(ctx) { /* override in subclasses */ } } export class Panel extends UIElement { constructor(x, y, w, h, title = '') { super(x, y, w, h); this.title = title; this.elements = []; } addElement(el) { this.elements.push(el); } draw(ctx) { ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(this.x, this.y, this.w, this.h); ctx.strokeStyle = '#fff'; ctx.strokeRect(this.x, this.y, this.w, this.h); if (this.title) { ctx.fillStyle = '#fff'; ctx.font = '16px monospace'; ctx.fillText(this.title, this.x + 10, this.y + 20); } this.elements.forEach(el => el.draw(ctx)); } handlePointerEvent(event, px, py) { super.handlePointerEvent(event, px, py); // panel itself this.elements.forEach(el => el.handlePointerEvent(event, px, py)); } handleKeyEvent(event) { this.elements.forEach(el => { if (el.onKeyDown) el.onKeyDown(event); }); } } export class Button extends UIElement { constructor(x, y, w, h, label, onClick) { super(x, y, w, h); this.label = label; this.onClick = onClick; } draw(ctx) { // ctx.fillStyle = this.active ? '#225433' : // this.hovered ? '#666' : '#333'; // ctx.fillRect(this.x, this.y, this.w, this.h); ctx.fillStyle = '#fff'; if (this.hovered) { ctx.fillStyle = '#666'; } ctx.font = '14px monospace'; ctx.fillText(this.label, this.x + 10, this.y + this.h - 8); } onPointerMove(px, py) { this.hovered = this.contains(px, py); //console.log(this); } onPointerUp(px, py) { const clicked = super.onPointerUp(px, py); if (clicked && this.onClick) this.onClick(); return clicked; } } export class Textbox extends UIElement { constructor(x, y, w, h, initialValue = '') { super(x, y, w, h); this.value = initialValue; this.focused = false; } draw(ctx) { ctx.fillStyle = '#000'; ctx.fillRect(this.x, this.y, this.w, this.h); ctx.strokeStyle = this.focused ? '#0f0' : '#aaa'; ctx.strokeRect(this.x, this.y, this.w, this.h); ctx.fillStyle = '#fff'; ctx.font = '14px monospace'; ctx.fillText(this.value, this.x + 5, this.y + this.h - 8); } onPointerUp(px, py) { const clicked = super.onPointerUp(px, py); this.focused = clicked; return clicked; } onKeyDown(event) { if (!this.focused) return; if (event.key >= '0' && event.key <= '9') { this.value += event.key; } else if (event.key === 'Backspace') { this.value = this.value.slice(0, -1); } } }