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("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("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); } } }