183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
import { point_inside_rec } from "$/common/utils.ts";
|
|
import { AssetManager } from "$/client/assets.ts";
|
|
import { InputManager } from "$/client/input_manager.ts";
|
|
import { draw_nine_slice } from "$/client/rendering/render_utils.ts";
|
|
import { draw_rect, draw_text, measure_text, Texture } from "$/client/renderer/mod.ts";
|
|
|
|
export const TEXT_SCALE = 2;
|
|
// how tall the font is at scale 1
|
|
export const TEXT_HEIGHT = 11;
|
|
|
|
const REPEAT_DELAY_MS = 400;
|
|
const REPEAT_RATE_MS = 40;
|
|
const CARET_BLINK_MS = 500;
|
|
|
|
// editing a line of text with the keyboard: typing, backspace (held repeats), delete, arrows, home and end
|
|
export class TextInput {
|
|
value: string;
|
|
caret: number;
|
|
max_length: number;
|
|
// which typed characters are kept
|
|
allowed: (char: string) => boolean;
|
|
|
|
#repeat_timer = 0;
|
|
|
|
constructor(value = "", max_length = 256, allowed: (char: string) => boolean = () => true) {
|
|
this.value = value;
|
|
this.caret = value.length;
|
|
this.max_length = max_length;
|
|
this.allowed = allowed;
|
|
}
|
|
|
|
// call once per frame while it has the keyboard
|
|
handle_keys() {
|
|
const now = performance.now();
|
|
let backspace = false;
|
|
if (InputManager.is_key_pressed("Backspace")) {
|
|
this.#repeat_timer = now;
|
|
backspace = true;
|
|
} else if (InputManager.is_key_down("Backspace") && now - this.#repeat_timer > REPEAT_DELAY_MS) {
|
|
this.#repeat_timer = now - (REPEAT_DELAY_MS - REPEAT_RATE_MS);
|
|
backspace = true;
|
|
}
|
|
if (backspace && this.caret > 0) {
|
|
this.value = this.value.slice(0, this.caret - 1) + this.value.slice(this.caret);
|
|
this.caret -= 1;
|
|
}
|
|
|
|
if (InputManager.is_key_pressed("Delete")) {
|
|
this.value = this.value.slice(0, this.caret) + this.value.slice(this.caret + 1);
|
|
}
|
|
if (InputManager.is_key_pressed("ArrowLeft")) {
|
|
this.caret = Math.max(0, this.caret - 1);
|
|
}
|
|
if (InputManager.is_key_pressed("ArrowRight")) {
|
|
this.caret = Math.min(this.value.length, this.caret + 1);
|
|
}
|
|
if (InputManager.is_key_pressed("Home")) {
|
|
this.caret = 0;
|
|
}
|
|
if (InputManager.is_key_pressed("End")) {
|
|
this.caret = this.value.length;
|
|
}
|
|
|
|
for (const char of InputManager.get_typed_characters()) {
|
|
if (this.value.length >= this.max_length || !this.allowed(char)) {
|
|
continue;
|
|
}
|
|
this.value = this.value.slice(0, this.caret) + char + this.value.slice(this.caret);
|
|
this.caret += 1;
|
|
}
|
|
}
|
|
|
|
// where the caret goes when the text starts at x
|
|
caret_x(x: number, scale = TEXT_SCALE) {
|
|
return x + measure_text(this.value.slice(0, this.caret), scale);
|
|
}
|
|
|
|
static caret_visible() {
|
|
return Math.floor(performance.now() / CARET_BLINK_MS) % 2 === 0;
|
|
}
|
|
}
|
|
|
|
// a clickable button, like minecraft's Button
|
|
export class Button {
|
|
x = 0;
|
|
y = 0;
|
|
width: number;
|
|
height: number;
|
|
label: string;
|
|
on_press: () => void;
|
|
active = true;
|
|
#hovered = false;
|
|
|
|
constructor(label: string, width: number, height: number, on_press: () => void) {
|
|
this.label = label;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.on_press = on_press;
|
|
}
|
|
|
|
// returns whether it was pressed
|
|
handle_input(): boolean {
|
|
const mouse = InputManager.get_mouse_position();
|
|
this.#hovered = this.active && point_inside_rec(mouse.x, mouse.y, this.x, this.y, this.width, this.height);
|
|
if (this.#hovered && InputManager.is_mouse_pressed(0)) {
|
|
InputManager.consume_mouse(0);
|
|
this.on_press();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
render() {
|
|
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
|
const [sx, sy] = this.#hovered ? [224, 16] : [176, 0];
|
|
const tint = this.active ? [1, 1, 1, 1] : [0.6, 0.6, 0.6, 1];
|
|
draw_nine_slice(ui, sx, sy, 16, 16, 4, 4, 4, 4, this.x, this.y, this.width, this.height, tint);
|
|
|
|
const text_width = measure_text(this.label, TEXT_SCALE);
|
|
draw_text(
|
|
this.label,
|
|
this.x + (this.width - text_width) / 2,
|
|
this.y + (this.height - TEXT_HEIGHT * TEXT_SCALE) / 2,
|
|
TEXT_SCALE,
|
|
this.active ? [1, 1, 1, 1] : [0.7, 0.7, 0.7, 1],
|
|
);
|
|
}
|
|
}
|
|
|
|
// a one line text field, like minecraft's EditBox. clicking it gives it the keyboard
|
|
export class EditBox {
|
|
x = 0;
|
|
y = 0;
|
|
width: number;
|
|
height: number;
|
|
input: TextInput;
|
|
focused = false;
|
|
// shown greyed out while it's empty
|
|
hint: string;
|
|
active = true;
|
|
|
|
constructor(width: number, height: number, input: TextInput, hint = "") {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.input = input;
|
|
this.hint = hint;
|
|
}
|
|
|
|
get value() {
|
|
return this.input.value;
|
|
}
|
|
|
|
// returns whether it was clicked, so the screen can move focus to it
|
|
handle_input(): boolean {
|
|
const mouse = InputManager.get_mouse_position();
|
|
const clicked = this.active && InputManager.is_mouse_pressed(0) &&
|
|
point_inside_rec(mouse.x, mouse.y, this.x, this.y, this.width, this.height);
|
|
if (clicked) {
|
|
InputManager.consume_mouse(0);
|
|
}
|
|
if (this.focused && this.active) {
|
|
this.input.handle_keys();
|
|
}
|
|
return clicked;
|
|
}
|
|
|
|
render() {
|
|
const ui = AssetManager.instance.get<Texture>("bworld:ui");
|
|
draw_nine_slice(ui, this.focused ? 320 : 304, 0, 16, 16, 4, 4, 4, 4, this.x, this.y, this.width, this.height);
|
|
|
|
const text_x = this.x + 10;
|
|
const text_y = this.y + (this.height - TEXT_HEIGHT * TEXT_SCALE) / 2;
|
|
if (this.value === "" && !this.focused) {
|
|
draw_text(this.hint, text_x, text_y, TEXT_SCALE, [0.6, 0.6, 0.6, 1]);
|
|
} else {
|
|
draw_text(this.value, text_x, text_y, TEXT_SCALE, this.active ? [1, 1, 1, 1] : [0.7, 0.7, 0.7, 1]);
|
|
}
|
|
if (this.focused && this.active && TextInput.caret_visible()) {
|
|
draw_rect(this.input.caret_x(text_x), text_y, 2, TEXT_HEIGHT * TEXT_SCALE);
|
|
}
|
|
}
|
|
}
|