211 lines
6.0 KiB
JavaScript
211 lines
6.0 KiB
JavaScript
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.numericOnly = options.numericOnly || false;
|
|
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;
|
|
|
|
if (e.key === "Backspace") {
|
|
this.value = 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.numericOnly) {
|
|
if (/[\d.\-]/.test(e.key)) {
|
|
this.value += e.key;
|
|
|
|
this.clampToRange(); // 👈 clamp on Enter
|
|
}
|
|
} else {
|
|
this.value += e.key;
|
|
|
|
this.clampToRange(); // 👈 clamp on Enter
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
update(dt) {
|
|
this.cursorTimer += dt;
|
|
if (this.cursorTimer > 500) {
|
|
this.cursorVisible = !this.cursorVisible;
|
|
this.cursorTimer = 0;
|
|
}
|
|
}
|
|
|
|
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.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];
|
|
}
|
|
}
|