Repository navigation
Expand file tree
/
Copy pathruntime.ts
More file actions
274 lines (241 loc) · 7.95 KB
/
Copy pathruntime.ts
File metadata and controls
274 lines (241 loc) · 7.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
namespace ui {
const UI_CONTROLLER_REPEAT_DELAY_MS = 250
const UI_CONTROLLER_REPEAT_INTERVAL_MS = 60
const UI_RUNTIME_FRAME_PRIORITY = 30
/**
* Display target that exposes a draw surface and presents frames.
*/
export interface UiDisplayAdapter {
/**
* Physical display surface for the next committed frame.
*/
surface: DrawSurface
/**
* Presents the current frame and returns the physical bitmap that was sent.
*/
commit(): Bitmap
}
/**
* Resolves optional bitmap and text assets by id.
*/
export interface UiAssetResolver {
/**
* Looks up a bitmap by asset id.
*
* Missing ids return a resolver-owned fallback bitmap by default. When
* `nullIfMissing` is `true`, missing ids may return `undefined`; callers
* must handle both bitmap and `undefined` results.
*/
getBitmap(
id: string | number,
nullIfMissing?: boolean,
): Bitmap | undefined
/**
* Returns display text for an asset id. Missing text returns the empty
* string.
*/
getText(id: string): string
}
class UiNoopAssetResolver implements UiAssetResolver {
private emptyBitmap_: Bitmap
constructor() {
this.emptyBitmap_ = bmp`.`
}
public getBitmap(
id: string | number,
nullIfMissing?: boolean,
): Bitmap | undefined {
if (nullIfMissing) return undefined
return this.emptyBitmap_
}
public getText(id: string): string {
return ""
}
}
/**
* Map entry: a bitmap, or a factory invoked to produce one.
*/
export type UiBitmapEntry = Bitmap | (() => Bitmap)
/**
* Asset resolver backed by a caller-supplied id-to-bitmap map. An entry may
* be a bitmap or a factory; a factory is invoked on each request. Missing
* ids return the fallback bitmap, or `undefined` when `nullIfMissing` is
* set.
*/
export class UiMapAssets implements UiAssetResolver {
private map_: { [key: string]: UiBitmapEntry }
private fallback_: Bitmap
constructor(map: { [key: string]: UiBitmapEntry }, fallback?: Bitmap) {
this.map_ = map
this.fallback_ = fallback
}
public getBitmap(
id: string | number,
nullIfMissing?: boolean,
): Bitmap | undefined {
const entry = this.map_[id]
if (entry === undefined)
return nullIfMissing ? undefined : this.fallback_
if (typeof entry == "function") return (<() => Bitmap>entry)()
return <Bitmap>entry
}
public getText(id: string): string {
return ""
}
}
/**
* Owns screen stack state, queued input, and frame execution.
*/
export class UiRuntime {
private display_: UiDisplayAdapter
private assets_: UiAssetResolver
private clearColor_: number
private stack_: UiScreenStack
private inputQueue_: UiInputEvent[]
private running_: boolean
private frameContext_: context.EventContext
private frameCallback_: context.FrameCallback
constructor(
display: UiDisplayAdapter,
assets?: UiAssetResolver,
clearColor?: number,
) {
this.display_ = display
this.assets_ = assets || new UiNoopAssetResolver()
this.clearColor_ = clearColor !== undefined ? clearColor : 0
this.inputQueue_ = []
this.stack_ = new UiScreenStack(this)
this.running_ = false
controller.setRepeatDefault(
UI_CONTROLLER_REPEAT_DELAY_MS,
UI_CONTROLLER_REPEAT_INTERVAL_MS,
)
}
/**
* Display target used for rendering and frame commit.
*/
public get display(): UiDisplayAdapter {
return this.display_
}
/**
* Asset resolver for screens and controls.
*/
public get assets(): UiAssetResolver {
return this.assets_
}
/**
* Stack that controls active screen lifecycle.
*/
public get screenStack(): UiScreenStack {
return this.stack_
}
/**
* Pushes a screen and makes it active.
*/
public push(screen: UiScreen): void {
this.stack_.push(screen)
this.bindFrameHandler()
}
/**
* Removes the active screen and reactivates the screen below it. Returns
* `undefined` when the stack is empty.
*/
public pop(): UiScreen | undefined {
const screen = this.stack_.pop()
this.bindFrameHandler()
return screen
}
/**
* Replaces the active screen without reactivating the screen below it.
* Returns `undefined` when the stack is empty.
*/
public replace(screen: UiScreen): UiScreen | undefined {
const replaced = this.stack_.replace(screen)
this.bindFrameHandler()
return replaced
}
/**
* Returns the active screen, or `undefined` when the stack is empty.
*/
public top(): UiScreen | undefined {
return this.stack_.top()
}
/**
* Returns the number of screens in the stack.
*/
public depth(): number {
return this.stack_.depth()
}
/**
* Queues an input event for the next frame.
*/
public dispatchInput(event: UiInputEvent): void {
this.inputQueue_.push(event)
}
/**
* Drops all queued input events.
*/
public clearInputQueue(): void {
while (this.inputQueue_.length) this.inputQueue_.pop()
}
/**
* Runs the UI from the current event context's frame callback.
*/
public start(): void {
this.running_ = true
this.bindFrameHandler()
}
/**
* Stops the runtime-owned frame callback.
*/
public stop(): void {
this.running_ = false
this.unbindFrameHandler()
}
/**
* Updates controller repeat, delivers queued input, updates, renders, and
* commits the active screen.
*/
public runFrame(): void {
this.updateControllerButtons()
this.stack_.runFrame(
this.inputQueue_,
this.display_,
this.clearColor_,
)
}
private updateControllerButtons(): void {
const dtms = (context.eventContext().deltaTime * 1000) | 0
controller.left.__update(dtms)
controller.right.__update(dtms)
controller.up.__update(dtms)
controller.down.__update(dtms)
}
private bindFrameHandler(): void {
if (!this.running_) return
const eventContext = context.eventContext()
if (!eventContext || !this.top()) {
this.unbindFrameHandler()
return
}
if (
this.frameContext_ == eventContext &&
this.frameCallback_ !== undefined
)
return
this.unbindFrameHandler()
this.frameContext_ = eventContext
this.frameCallback_ = eventContext.registerFrameHandler(
UI_RUNTIME_FRAME_PRIORITY,
() => this.runFrame(),
)
}
private unbindFrameHandler(): void {
if (this.frameContext_ && this.frameCallback_) {
this.frameContext_.unregisterFrameHandler(this.frameCallback_)
}
this.frameContext_ = undefined
this.frameCallback_ = undefined
}
}
}