Skip to content

Instantly share code, notes, and snippets.

@farism
Created June 23, 2023 08:26
Show Gist options
  • Save farism/1bfe95e15ab3a80b2b34628058ce456b to your computer and use it in GitHub Desktop.
Save farism/1bfe95e15ab3a80b2b34628058ce456b to your computer and use it in GitHub Desktop.
Input.tsx
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