Skip to content

Commit 3bb3191

Browse files
committed
Move InputField implementation from nax-ccuilib to here
1 parent 7c73ab7 commit 3bb3191

9 files changed

Lines changed: 366 additions & 37 deletions

File tree

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"fuzzysort": "^2.0.4",
2525
"jszip": "^3.10.1",
2626
"marked": "=12.0.2",
27-
"nax-ccuilib": "github:krypciak/nax-ccuilib",
2827
"prettier": "3.2.4",
2928
"rimraf": "^3.0.2",
3029
"typescript": "^5.4.5",

pnpm-lock.yaml

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export {}
2+
declare global {
3+
namespace modmanager.gui {
4+
interface InputFieldCursor extends ig.GuiElementBase {
5+
colour: string
6+
cursorTick: number
7+
movingTimer: number
8+
active: boolean
9+
}
10+
11+
interface InputFieldCursorCon extends ImpactClass<InputFieldCursor> {
12+
new (colour: string): InputFieldCursor
13+
}
14+
15+
let InputFieldCursor: InputFieldCursorCon
16+
}
17+
}
18+
19+
modmanager.gui.InputFieldCursor = ig.GuiElementBase.extend({
20+
colour: 'red',
21+
cursorTick: 0,
22+
active: false,
23+
movingTimer: 0,
24+
25+
init(colour) {
26+
this.parent()
27+
this.colour = colour
28+
this.hook.size.x = 1
29+
this.hook.size.y = 11
30+
this.active = false
31+
},
32+
33+
updateDrawables(renderer) {
34+
if (this.active) {
35+
this.cursorTick = (this.cursorTick + ig.system.actualTick) % 1
36+
this.movingTimer -= ig.system.actualTick
37+
if (this.movingTimer > 0 || this.cursorTick > 0.5) {
38+
renderer.addColor(this.colour, this.hook.pos.x, this.hook.pos.y, this.hook.size.x, this.hook.size.y)
39+
}
40+
}
41+
},
42+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export {}
2+
declare global {
3+
namespace modmanager.gui {
4+
interface InputFieldType {
5+
height: number
6+
ninepatch: ig.NinePatch
7+
highlight: sc.ButtonGui.Highlight
8+
}
9+
10+
let INPUT_FIELD_TYPE: { [index: string]: InputFieldType }
11+
}
12+
}
13+
modmanager.gui.INPUT_FIELD_TYPE = {}
14+
modmanager.gui.INPUT_FIELD_TYPE.DEFAULT = {
15+
height: 20,
16+
ninepatch: new ig.NinePatch('media/gui/buttons.png', {
17+
width: 13,
18+
height: 18,
19+
left: 1,
20+
top: 1,
21+
right: 2,
22+
bottom: 2,
23+
offsets: {
24+
default: {
25+
x: 184,
26+
y: 24,
27+
},
28+
focus: {
29+
x: 184,
30+
y: 24,
31+
},
32+
pressed: {
33+
x: 184,
34+
y: 24,
35+
},
36+
},
37+
}),
38+
highlight: {
39+
startX: 200,
40+
endX: 215,
41+
leftWidth: 2,
42+
rightWidth: 2,
43+
offsetY: 24,
44+
gfx: new ig.Image('media/gui/buttons.png'),
45+
pattern: new ig.ImagePattern('media/gui/buttons.png', 202, 24, 11, 20, ig.ImagePattern.OPT.REPEAT_X),
46+
},
47+
}

src/gui/input-field/input-field.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import './input-field-cursor'
2+
import './input-field-type'
3+
4+
declare global {
5+
namespace modmanager.gui {
6+
interface InputField extends ig.FocusGui {
7+
gfx: ig.Image
8+
value: string[]
9+
bg: sc.ButtonBgGui
10+
focusTimer: number
11+
alphaTimer: number
12+
animateOnPress: boolean
13+
noFocusOnPressed: boolean
14+
submitSound: ig.Sound
15+
blockedSound: ig.Sound
16+
type: modmanager.gui.InputFieldType
17+
boundProcessInput: (this: Window, ev: KeyboardEvent) => any
18+
validChars: RegExp
19+
onCharacterInput: (value: string, key: string) => any
20+
dummyForClipping: sc.DummyContainer
21+
highlight: sc.ButtonHighlightGui
22+
textChild: sc.TextGui
23+
cursorTick: number
24+
cursorPos: number
25+
cursor: InputFieldCursor
26+
obscure: boolean
27+
obscureChar: string
28+
29+
calculateCursorPos(this: this): number
30+
getValueAsString(this: this): string
31+
processInput(this: this, event: KeyboardEvent): void
32+
setTextChildText(this: this, text: string): void
33+
setText(this: this, text: string): void
34+
unsetFocus(this: this): void
35+
updateCursorPos(this: this, delta: number): void
36+
setObscure(this: this, obscure: boolean): void
37+
}
38+
39+
interface InputFieldCon extends ImpactClass<InputField> {
40+
new (
41+
width: number,
42+
height: number,
43+
type?: modmanager.gui.InputFieldType,
44+
obscure?: boolean,
45+
obscureChar?: string
46+
): InputField
47+
}
48+
49+
let InputField: InputFieldCon
50+
}
51+
}
52+
53+
modmanager.gui.InputField = ig.FocusGui.extend({
54+
gfx: new ig.Image('media/gui/buttons.png'),
55+
value: [],
56+
bg: null,
57+
focusTimer: 0,
58+
alphaTimer: 0,
59+
animateOnPress: false,
60+
noFocusOnPressed: false,
61+
submitSound: sc.BUTTON_SOUND.submit,
62+
blockedSound: sc.BUTTON_SOUND.denied,
63+
type: null,
64+
boundProcessInput: null,
65+
validChars: /[a-zA-Z0-9,! ]*/,
66+
cursorPos: 0,
67+
onCharacterInput: undefined,
68+
dummyForClipping: null,
69+
cursorTick: 0,
70+
cursor: undefined,
71+
obscure: false,
72+
obscureChar: '*',
73+
init(width: number, height: number, type?: modmanager.gui.InputFieldType, obscure?: boolean, obscureChar?: string) {
74+
this.parent(true)
75+
this.setSize(width, height)
76+
77+
this.obscure = obscure || false
78+
this.obscureChar = obscureChar || '*'
79+
80+
this.hook.clip = true
81+
82+
this.type = type || modmanager.gui.INPUT_FIELD_TYPE.DEFAULT
83+
84+
this.bg = new sc.ButtonBgGui(this.hook.size.x, this.type)
85+
this.bg.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_TOP)
86+
this.bg.hook.size = this.hook.size
87+
this.addChildGui(this.bg)
88+
89+
this.highlight = new sc.ButtonHighlightGui(this.hook.size.x, this.type)
90+
this.addChildGui(this.highlight)
91+
92+
this.textChild = new sc.TextGui(this.value, {
93+
speed: ig.TextBlock.SPEED.IMMEDIATE,
94+
})
95+
96+
this.textChild.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_TOP)
97+
98+
// #region dummy
99+
this.dummyForClipping = new sc.DummyContainer(this.textChild)
100+
this.dummyForClipping.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_TOP)
101+
this.dummyForClipping.setPos(4, 1)
102+
this.dummyForClipping.setSize(width - 8, height)
103+
this.addChildGui(this.dummyForClipping)
104+
// #endregion
105+
106+
// #region cursor
107+
this.cursor = new modmanager.gui.InputFieldCursor('#FF6D00')
108+
this.cursor.hook.pos.y = 2
109+
// Set initial cursor position.
110+
this.cursor.hook.pos.x = this.calculateCursorPos()
111+
this.addChildGui(this.cursor)
112+
// #endregion
113+
114+
this.boundProcessInput = this.processInput.bind(this)
115+
this.validChars = /[a-zA-Z0-9,! ]*/
116+
},
117+
118+
focusGained() {
119+
this.parent()
120+
ig.input.ignoreKeyboard = true
121+
for (const action of Object.keys(ig.input.actions) as ig.Input.KnownAction[]) ig.input.actions[action] = false
122+
this.cursor.active = true
123+
window.addEventListener('keydown', this.boundProcessInput, false)
124+
},
125+
126+
focusLost() {
127+
this.parent()
128+
ig.input.ignoreKeyboard = false
129+
this.cursor.active = false
130+
window.removeEventListener('keydown', this.boundProcessInput)
131+
},
132+
133+
processInput(event: KeyboardEvent) {
134+
event.preventDefault()
135+
switch (event.code) {
136+
case 'ArrowLeft':
137+
this.updateCursorPos(-1)
138+
break
139+
case 'ArrowRight':
140+
this.updateCursorPos(1)
141+
break
142+
case 'Home':
143+
this.cursorPos = 0
144+
break
145+
case 'End':
146+
this.cursorPos = this.value.length
147+
break
148+
default: {
149+
let old = this.getValueAsString()
150+
151+
if (event.key.length === 1 && this.validChars.test(event.key)) {
152+
this.value.splice(this.cursorPos, 0, event.key)
153+
this.updateCursorPos(1)
154+
} else if (event.code === 'Backspace' && this.value.length > 0 && this.cursorPos !== 0) {
155+
// Backspace
156+
this.value.splice(this.cursorPos - 1, 1)
157+
this.updateCursorPos(-1)
158+
} else if (event.code === 'Delete' && this.value.length > 0 && this.cursorPos !== this.value.length) {
159+
this.value.splice(this.cursorPos, 1)
160+
}
161+
162+
let text = this.getValueAsString()
163+
if (text !== old) {
164+
this.setTextChildText(text)
165+
166+
if (this.onCharacterInput) {
167+
this.onCharacterInput(text, event.key)
168+
}
169+
}
170+
171+
this.cursor.movingTimer = 1
172+
173+
break
174+
}
175+
}
176+
177+
this.cursor.hook.pos.x = this.calculateCursorPos()
178+
},
179+
180+
setTextChildText(text: string) {
181+
if (this.obscure) {
182+
this.textChild.setText(this.obscureChar.repeat(this.value.length))
183+
} else {
184+
this.textChild.setText(text)
185+
}
186+
},
187+
188+
setText(text: string) {
189+
this.setTextChildText(text)
190+
this.value = text.split('')
191+
this.cursorPos = text.length
192+
this.cursor.hook.pos.x = this.calculateCursorPos()
193+
},
194+
195+
getValueAsString() {
196+
return this.value.join('')
197+
},
198+
199+
updateCursorPos(delta) {
200+
this.cursorPos += delta
201+
this.cursorPos = Math.min(Math.max(this.cursorPos, 0), this.value.length)
202+
},
203+
204+
calculateCursorPos() {
205+
let value = this.obscure
206+
? this.obscureChar.repeat(this.cursorPos)
207+
: this.value.slice(0, this.cursorPos).join('')
208+
return this.textChild.textBlock.font.getTextDimensions(value, this.textChild.textBlock.linePadding).x / 2 + 1.5
209+
},
210+
211+
setObscure(obscure) {
212+
this.obscure = obscure
213+
this.setTextChildText(this.getValueAsString())
214+
},
215+
216+
// Liberated from ButtonGui
217+
update() {
218+
this.parent()
219+
220+
if (this.keepPressed && this.pressed && this.animateOnPress) {
221+
// If this element is currently focussed
222+
if (this.focus) {
223+
this.alphaTimer = (this.alphaTimer + ig.system.actualTick) % 1
224+
} else {
225+
this.alphaTimer = 0
226+
this.focusTimer = 0.1
227+
}
228+
} else if (this.keepPressed && this.pressed && !this.noFocusOnPressed) {
229+
this.focusTimer = this.focusTimer + ig.system.actualTick
230+
if (this.focusTimer > 0.1) this.focusTimer = 0.1 // This line is made redundant by this.focusTimer.limit(0, 0.1);
231+
this.alphaTimer = 0
232+
} else if (this.focus && this.focusTimer < 0.1) {
233+
// If we are focussing and the focus timer is less than max, increase the focus timer
234+
this.focusTimer = this.focusTimer + ig.system.actualTick
235+
this.alphaTimer = 0
236+
} else if (!this.focus && this.focusTimer > 0) {
237+
// If we are no longer focussing, reduce the focus timer
238+
this.focusTimer = this.focusTimer - ig.system.actualTick
239+
this.alphaTimer = 0
240+
} else {
241+
this.alphaTimer = (this.alphaTimer + ig.system.actualTick) % 1
242+
}
243+
this.focusTimer.limit(0, 0.1)
244+
this.bg.currentTileOffset = this.keepPressed && this.pressed ? 'pressed' : this.focus ? 'focus' : 'default'
245+
if (this.highlight) {
246+
this.highlight.focusWeight = this.focusTimer / 0.1
247+
var a = this.alphaTimer / 1,
248+
a = KEY_SPLINES.EASE_IN_OUT.get(1 - (a > 0.5 ? 1 - (a - 0.5) * 2 : a * 2)),
249+
a = 0.8 * a + 0.2
250+
this.active || (a = a * 0.5)
251+
this.highlight.hook.localAlpha = a
252+
}
253+
},
254+
255+
unsetFocus() {
256+
this.focus = false
257+
this.setPressed(false)
258+
if (this.highlight) {
259+
this.highlight.hook.localAlpha = 0
260+
this.highlight.focusWeight = 0
261+
}
262+
this.focusTimer = 0
263+
this.alphaTimer = 0
264+
},
265+
})

0 commit comments

Comments
 (0)