Created
June 23, 2023 08:26
-
-
Save farism/1bfe95e15ab3a80b2b34628058ce456b to your computer and use it in GitHub Desktop.
Input.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Jsx from "../Jsx" | |
import { InputActionId } from "../Document" | |
import { SCALING_FACTOR } from "../constants" | |
import { color, onTick, vec3 } from "../utils" | |
const REPEAT_RATE = 0.075 | |
const stylesheet: Stylesheet = { | |
input: { | |
alignItems: "stretch", | |
justifyContent: "start", | |
flexDirection: "row", | |
width: 240, | |
height: 48, | |
paddingLeft: 8, | |
paddingRight: 8, | |
texture: "kenny", | |
flipbook: "grey_button05", | |
}, | |
mask: { | |
grow: 1, | |
color: color(0, 0, 0, 0), | |
overflow: "hidden", | |
alignItems: "start", | |
paddingLeft: 4, | |
paddingRight: 4, | |
}, | |
container: { | |
grow: 1, | |
alignItems: "start", | |
justifyContent: "start", | |
color: color(0, 0, 0, 1), | |
pivot: gui.PIVOT_W, | |
}, | |
text: { | |
grow: 1, | |
font: "inter", | |
color: color(0, 0, 0, 1), | |
pivot: gui.PIVOT_W, | |
}, | |
cursor: { | |
grow: 1, | |
flexPosition: "absolute", | |
visible: false, | |
width: 2, | |
height: 32, | |
left: -2, | |
top: 8, | |
color: color(0, 0, 0, 0.5), | |
}, | |
selection: { | |
grow: 1, | |
flexPosition: "absolute", | |
left: -2, | |
height: 32, | |
top: 8, | |
pivot: gui.PIVOT_W, | |
color: color(0, 0, 0, 0.5), | |
}, | |
} | |
export interface InputProps { | |
ref?: (n: Input) => void | |
value?: string | |
} | |
export class Input extends Jsx.Component<InputProps> { | |
nodes: Jsx.Nodes<"input" | "mask" | "container" | "selection" | "text" | "cursor"> = {} | |
clickTime: number = 0 | |
containerOriginPos: vmath.vector3 = vec3() | |
cursorBlinkTimer: any | |
cursorIndex: number = 0 | |
cursorOriginPos: vmath.vector3 = vec3() | |
focused: boolean = false | |
font: any | |
ref?: (n: Input) => void | |
repeatTime: number = 0 | |
selecting: boolean = false | |
selectionIndex: number | null = null | |
shiftKey: boolean = false | |
superKey: boolean = false | |
textMetrics: number[] = [] | |
value: string | |
words: string[] = [] | |
private get document() { | |
return this.nodes.container?.document | |
} | |
constructor(props: InputProps) { | |
super(props) | |
this.value = props.value?.toString() ?? "" | |
onTick(this.initialize) | |
} | |
initialize = () => { | |
const { text, container, cursor } = this.nodes | |
if (text && container && cursor) { | |
this.font = gui.get_font(text.node) | |
text.text = this.value | |
this.containerOriginPos = container.position | |
this.cursorOriginPos = cursor.position | |
this.generateTextMetrics() | |
this.generateWords() | |
} | |
} | |
render = () => { | |
return ( | |
<box ref={(n) => (this.nodes.input = n)} style={stylesheet.input} onMouseDown={this.onMouseDown}> | |
<box ref={(n) => (this.nodes.mask = n)} style={stylesheet.mask}> | |
<box ref={(n) => (this.nodes.container = n)} style={stylesheet.container} inheritAlpha={false}> | |
<box ref={(n) => (this.nodes.cursor = n)} style={stylesheet.cursor} inheritAlpha={false} /> | |
<box ref={(n) => (this.nodes.selection = n)} style={stylesheet.selection} inheritAlpha={false} /> | |
<text ref={(n) => (this.nodes.text = n)} style={stylesheet.text} inheritAlpha={false} /> | |
</box> | |
</box> | |
</box> | |
) | |
} | |
clamp = (i: number) => { | |
return math.max(0, math.min(i, string.len(this.value))) | |
} | |
getTextMetricWidth = (text: string) => { | |
return gui.get_text_metrics(this.font, text, 0, false, 0, 0).width | |
} | |
generateTextMetrics = () => { | |
this.textMetrics = [] | |
for (let i = 0; i <= this.value.length; i++) { | |
this.textMetrics.push(this.getTextMetricWidth(this.value.slice(0, i))) | |
} | |
} | |
generateWords = () => { | |
this.words = this.value.split(" ") | |
} | |
shouldApplyAction = (action: any, repeatDelay: number) => { | |
const time = socket.gettime() | |
if (action.pressed) { | |
this.repeatTime = time + 0.3 | |
return true | |
} | |
if (time - this.repeatTime > repeatDelay) { | |
this.repeatTime = time | |
return true | |
} | |
return false | |
} | |
updateCursor = (navigating: boolean = false) => { | |
const { mask, container, cursor } = this.nodes | |
if (mask && container && cursor) { | |
const width = this.textMetrics[this.cursorIndex] ?? 0 | |
const cursorPos = (this.cursorOriginPos + vec3(width + 1, 0, 0)) as vmath.vector3 | |
cursor.position = cursorPos | |
const maskWidth = mask.size.x | |
const containerPos = container.position | |
const translateX = this.containerOriginPos.x - containerPos.x | |
if (cursorPos.x > maskWidth + translateX - 10) { | |
container.position = vec3(this.containerOriginPos.x + (maskWidth - cursorPos.x) - 10, 0, 0) | |
} else if (cursor.x < translateX + 10) { | |
container.position = vec3(this.containerOriginPos.x - cursorPos.x + (this.cursorIndex === 0 ? 0 : 10), 0, 0) | |
} | |
this.startCursorBlink() | |
} | |
} | |
updateSelection = () => { | |
const { selection, cursor } = this.nodes | |
if (selection && cursor) { | |
if (this.selectionIndex === null) { | |
selection.visible = false | |
} else { | |
selection.visible = true | |
const start = this.selectionIndex >= this.cursorIndex ? this.cursorIndex : this.selectionIndex | |
const end = this.selectionIndex >= this.cursorIndex ? this.selectionIndex : this.cursorIndex | |
const width = this.textMetrics[end] - this.textMetrics[start] | |
selection.size = vec3(width, selection.size.y, 0) | |
selection.pivot = this.selectionIndex >= this.cursorIndex ? gui.PIVOT_W : gui.PIVOT_E | |
selection.position = cursor.position | |
} | |
} | |
} | |
alterText = (str: string) => { | |
let start = this.cursorIndex | |
let end = this.cursorIndex | |
if (this.selectionIndex !== null) { | |
start = math.min(this.selectionIndex, this.cursorIndex) | |
end = math.max(this.selectionIndex, this.cursorIndex) | |
} | |
this.value = this.value.slice(0, start) + str + this.value.slice(end) | |
if (this.nodes.text) { | |
this.nodes.text.text = this.value | |
} | |
} | |
getSelectedText = () => { | |
if (this.selectionIndex !== null) { | |
const start = math.min(this.selectionIndex, this.cursorIndex) | |
const end = math.max(this.selectionIndex, this.cursorIndex) | |
return this.value.slice(start, end) | |
} | |
return "" | |
} | |
onText = (action: any) => { | |
this.alterText(action.text) | |
this.cursorIndex = this.cursorIndex + string.len(action.text) | |
this.selectionIndex = null | |
this.generateTextMetrics() | |
this.updateCursor() | |
this.updateSelection() | |
} | |
onBackspace = (action: any) => { | |
if (!this.shouldApplyAction(action, REPEAT_RATE)) { | |
return | |
} | |
if (this.nodes.text) { | |
if (this.selectionIndex !== null) { | |
this.alterText("") | |
if (this.cursorIndex >= this.selectionIndex) { | |
this.cursorIndex = this.selectionIndex | |
} | |
} else if (this.cursorIndex > 0) { | |
this.value = this.value.slice(0, this.cursorIndex - 1) + this.value.slice(this.cursorIndex) | |
this.nodes.text.text = this.value | |
this.cursorIndex = this.clamp(this.cursorIndex - 1) | |
} | |
this.selectionIndex = null | |
this.generateTextMetrics() | |
this.updateCursor() | |
this.updateSelection() | |
} | |
} | |
onLeft = (action: any) => { | |
if (!this.shouldApplyAction(action, REPEAT_RATE)) { | |
return | |
} | |
let newCursorIndex = this.clamp(this.cursorIndex - 1) | |
if (this.shiftKey && this.nodes.selection) { | |
this.selectionIndex = this.selectionIndex === null ? this.cursorIndex : this.selectionIndex | |
} else { | |
if (this.selectionIndex !== null) { | |
newCursorIndex = math.min(this.cursorIndex, this.selectionIndex) | |
} | |
this.selectionIndex = null | |
} | |
this.cursorIndex = this.superKey ? 0 : newCursorIndex | |
this.updateCursor(true) | |
this.updateSelection() | |
} | |
onRight = (action: any) => { | |
if (!this.shouldApplyAction(action, REPEAT_RATE)) { | |
return | |
} | |
let newCursorIndex = this.clamp(this.cursorIndex + 1) | |
if (this.shiftKey && this.nodes.selection) { | |
this.selectionIndex = this.selectionIndex === null ? this.cursorIndex : this.selectionIndex | |
} else { | |
if (this.selectionIndex !== null) { | |
newCursorIndex = math.max(this.cursorIndex, this.selectionIndex) | |
} | |
this.selectionIndex = null | |
} | |
this.cursorIndex = this.superKey ? this.clamp(string.len(this.value)) : newCursorIndex | |
this.updateCursor(true) | |
this.updateSelection() | |
} | |
onSelectAll = (action: any) => { | |
if (this.superKey) { | |
this.selectionIndex = 0 | |
this.cursorIndex = this.clamp(this.value.length) | |
this.updateCursor() | |
this.updateSelection() | |
} | |
} | |
onCopy = (action: any) => { | |
if (action.pressed) { | |
clipboard.copy(this.getSelectedText()) | |
} | |
} | |
onPaste = (action: any) => { | |
if (action.pressed) { | |
this.onText({ | |
text: clipboard.paste(), | |
}) | |
} | |
} | |
cancelCursorTimer = () => { | |
if (this.cursorBlinkTimer) { | |
timer.cancel(this.cursorBlinkTimer) | |
} | |
} | |
startCursorBlink = () => { | |
if (this.nodes.cursor) { | |
this.nodes.cursor.visible = true | |
this.cancelCursorTimer() | |
} | |
this.cursorBlinkTimer = timer.delay(0.5, true, () => { | |
if (this.nodes.cursor) { | |
this.nodes.cursor.visible = !this.nodes.cursor.visible | |
} | |
}) | |
} | |
stopCursorBlink = () => { | |
if (this.nodes.cursor) { | |
this.nodes.cursor.visible = false | |
} | |
this.cancelCursorTimer() | |
} | |
onInput = (actionId: InputActionId, action: any) => { | |
if (actionId === "text") { | |
this.onText(action) | |
} else if (actionId === "backspace") { | |
this.onBackspace(action) | |
} else if (actionId === "shift") { | |
this.onShift(action) | |
} else if (actionId === "super") { | |
this.onSuper(action) | |
} else if (actionId === "left") { | |
this.onLeft(action) | |
} else if (actionId === "right") { | |
this.onRight(action) | |
} else if (actionId === "a") { | |
this.onSelectAll(action) | |
} else if (actionId === "c") { | |
this.onCopy(action) | |
} else if (actionId === "v") { | |
this.onPaste(action) | |
} else if (actionId === "mousedown") { | |
this.onDocumentMouseDown(action) | |
} else if (actionId === "mouseup") { | |
this.onDocumentMouseUp(action) | |
} else if (actionId === "mousemove") { | |
this.onDocumentMouseMove(action) | |
} | |
} | |
onShift = (action: any) => { | |
if (action.pressed) { | |
this.shiftKey = true | |
} else if (action.released) { | |
this.shiftKey = false | |
} | |
} | |
onSuper = (action: any) => { | |
if (action.pressed) { | |
this.superKey = true | |
} else if (action.released) { | |
this.superKey = false | |
} | |
} | |
addListeners = () => { | |
this.document?.addInputListener(this.onInput) | |
} | |
removeListeners = () => { | |
this.document?.removeInputListener(this.onInput) | |
} | |
focus = () => { | |
this.focused = true | |
this.startCursorBlink() | |
this.removeListeners() | |
this.addListeners() | |
} | |
unfocus = () => { | |
this.focused = false | |
this.selectionIndex = null | |
this.stopCursorBlink() | |
this.updateSelection() | |
this.removeListeners() | |
} | |
getValue = () => { | |
return this.nodes.text?.text || "" | |
} | |
onDocumentMouseDown = (action: any) => { | |
if (action.node !== this.nodes.input) { | |
this.unfocus() | |
} | |
} | |
onDocumentMouseUp = (action: any) => { | |
this.selecting = false | |
} | |
onDocumentMouseMove = (action: any) => { | |
if (this.selecting) { | |
if (this.selectionIndex === null) { | |
this.selectionIndex = this.cursorIndex | |
} | |
this.setCursorIndex(action.screen_x) | |
this.updateSelection() | |
} | |
} | |
setCursorIndex = (screenX: number) => { | |
if (this.nodes.text) { | |
const p = this.nodes.text.screenPosition | |
const mouseX = screenX / SCALING_FACTOR | |
const textX = p.x / SCALING_FACTOR | |
const x = mouseX - textX | |
let index = this.textMetrics.findIndex((mx) => mx > x) | |
if (index >= 0) { | |
const char = this.value.slice(index, index + 1) | |
const charX = this.textMetrics[index] | |
const charWidth = this.getTextMetricWidth(char) | |
this.cursorIndex = x - charX > charWidth / 2 ? index + 1 : index | |
this.updateCursor() | |
} | |
} | |
} | |
onMouseDown = (action: any) => { | |
const time = socket.gettime() | |
this.selectionIndex = null | |
this.selecting = true | |
action.node = this.nodes.input | |
this.setCursorIndex(action.screen_x) | |
// if(time < clickTime + 0.3) { | |
// } | |
// clickTime = time | |
this.updateSelection() | |
this.focus() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment