export class CanvasTextInput { constructor(offsetX, offsetY, width = 120, value = "", label = "", options = {}) { this.x = 0; this.y = 0; this.offsetX = offsetX; this.offsetY = offsetY; this.width = width; this.height = 24; this.labelOffset = 16; // space above input for label this.value = String(value); //this.placeholder = placeholder; this.focused = false; this.cursorVisible = false; this.cursorTimer = 0; this.mode = options.mode || "float"; // "int" or "float" this.min = options.min ?? -Infinity; this.max = options.max ?? Infinity; this.label = label || ""; } draw(ctx) { if (this.label) { ctx.fillStyle = "#000"; ctx.font = "13px sans-serif"; ctx.fillText(this.label, this.x, this.y - this.labelOffset + 15); } ctx.fillStyle = "#fff"; ctx.strokeStyle = "#666"; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.strokeRect(this.x, this.y, this.width, this.height); ctx.fillStyle = "#000"; ctx.font = "14px sans-serif"; const text = this.value || this.placeholder; ctx.fillText(text, this.x + 5, this.y + 15); if (this.focused && this.cursorVisible) { const textWidth = ctx.measureText(this.value).width; ctx.beginPath(); ctx.moveTo(this.x + 5 + textWidth, this.y + 4); ctx.lineTo(this.x + 5 + textWidth, this.y + this.height - 4); ctx.strokeStyle = "#000"; ctx.stroke(); } } contains(mx, my) { return mx >= this.x && mx <= this.x + this.width && my >= this.y && my <= this.y + this.height; } handleClick(mx, my) { console.log("HERE", mx, my); this.focused = this.contains(mx, my); return this.focused; } handleKey(e) { if (!this.focused) return false; console.log(e); if (e.key === "Backspace") { this.value = String(this.value).slice(0, -1); } else if (e.key === "Enter") { this.clampToRange(); // 👈 clamp on Enter this.focused = false; } else if (e.key.length === 1) { if (this.mode === "int") { if (/[\d\-]/.test(e.key)) { this.value += e.key; } } else if (this.mode === "float") { if (/[\d.\-]/.test(e.key)) { this.value += e.key; } } } return true; } update(dt) { this.cursorTimer += dt; if (this.cursorTimer > 500) { this.cursorVisible = !this.cursorVisible; this.cursorTimer = 0; } } handleMouseMove(mx, my) { // Optional: highlight or cursor change return this.contains(mx, my); } clampToRange() { let val = parseFloat(this.value); if (isNaN(val)) val = 0; val = Math.min(this.max, Math.max(this.min, val)); this.value = String(val); } get numericValue() { let val = parseFloat(this.value); if (isNaN(val)) return 0; return Math.min(this.max, Math.max(this.min, val)); } } export class CanvasDropdown { constructor(offsetX, offsetY, width, items = [], selected = 0) { this.x = 0; this.y = 0 this.offsetX = offsetX; this.offsetY = offsetY; this.width = width; this.height = 20; this.items = items; this.selectedIndex = 0; this.open = false; this.hoverIndex = -1; } draw(ctx) { // Collapsed box ctx.fillStyle = "#eee"; ctx.strokeStyle = "#666"; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.strokeRect(this.x, this.y, this.width, this.height); ctx.fillStyle = "#000"; ctx.font = "14px sans-serif"; ctx.fillText(this.items[this.selectedIndex], this.x + 5, this.y + 15); // Expanded list if (this.open) { for (let i = 0; i < this.items.length; i++) { const itemY = this.y + this.height + i * this.height; ctx.fillStyle = i === this.hoverIndex ? "#cce" : "#fff"; ctx.fillRect(this.x, itemY, this.width, this.height); ctx.strokeRect(this.x, itemY, this.width, this.height); ctx.fillStyle = "#000"; ctx.fillText(this.items[i], this.x + 5, itemY + 15); } } } contains(mx, my) { const totalHeight = this.height + (this.open ? this.items.length * this.height : 0); return mx >= this.x && mx <= this.x + this.width && my >= this.y && my <= this.y + totalHeight; } handleClick(mx, my) { if (this.open) { for (let i = 0; i < this.items.length; i++) { const itemY = this.y + this.height + i * this.height; if (mx >= this.x && mx <= this.x + this.width && my >= itemY && my <= itemY + this.height) { this.selectedIndex = i; this.selected = this.items[i]; // 👈 This line is missing this.open = false; this.hoverIndex = -1; return true; } } this.open = false; this.hoverIndex = -1; return true; } if (mx >= this.x && mx <= this.x + this.width && my >= this.y && my <= this.y + this.height) { this.open = true; return true; } return false; } handleMouseMove(mx, my) { if (!this.open) return false; let hovered = -1; for (let i = 0; i < this.items.length; i++) { const itemY = this.y + this.height + i * this.height; if (mx >= this.x && mx <= this.x + this.width && my >= itemY && my <= itemY + this.height) { hovered = i; break; } } if (hovered !== this.hoverIndex) { this.hoverIndex = hovered; return true; } return false; } get selectedValue() { return this.items[this.selectedIndex]; } }