/spacey

To get this branch, use:
bzr branch http://9ix.org/bzr/spacey
1 by Josh C
zoetrope 1.4
1
-- Class: App 
2
-- An app is where all the magic happens. :) It contains a 
3
-- view, the group where all major action happens, as well as the
4
-- meta view, which persists across views. Only one app may run at
5
-- a time.
6
--
7
-- An app's job is to get things up and running -- most of its logic
8
-- lives in its onRun handler, but for a simple app, you can also
9
-- use the onUpdate handler instead of writing a custom <View>.
10
-- 
11
-- Once an app has begun running, it may be accessed globally via
12
-- <the>.app.
13
--
14
-- Extends:
15
--  	<Class>
16
--
17
-- Event: onRun
18
-- 		Called once, when the app begins running.
19
--
20
-- Event: onEnterFullscreen
21
--		Called after entering fullscreen successfully.
22
--
23
-- Event: onExitFullscreen
24
--		Called after exiting fullscreen successfully.
25
26
App = Class:extend
27
{
28
	-- Property: name
29
	-- This is shown in the window title bar.
30
	name = 'Zoetrope',
31
	
32
	-- Property: icon
33
	-- A path to an image to use as the window icon (a 32x32 PNG is recommended).
34
	-- This doesn't affect the actual executable's icon in the taskbar or dock. 
35
36
	-- Property: fps
37
	-- Maximum frames per second requested. In practice, your
38
	-- FPS may vary from frame to frame. Every event handler (e.g. onUpdate)
39
	-- is passed the exact elapsed time in seconds.
40
	fps = 60,
41
	
42
	-- Property: timeScale
43
	-- Multiplier for elapsed time; 1.0 is normal, 0 is completely frozen.
44
	timeScale = 1,
45
	
46
	-- Property: active
47
	-- If false, nothing receives update-related events, including the meta view.
48
	-- These events specifically are onStartFrame, onUpdate, and onEndFrame.
49
	active = true,
50
51
	-- Property: deactivateOnBlur
52
	-- Should the app automatically set its active property to false when its
53
	-- window loses focus?
54
	deactivateOnBlur = true,
55
	
56
	-- Property: view
57
	-- The current <View>. When the app is running, this is also accessible
58
	-- globally via <the>.view. In order to switch views, you must set this
59
	-- property, *not* <the>.view.
60
61
	-- Property: meta
62
	-- A <Group> that persists across all views during the app's lifespan.
63
64
	-- Property: keys
65
	-- A <Keys> object that listens to the keyboard. When the app is running, this
66
	-- is also accessible globally via <the>.keys.
67
68
	-- Property: width
69
	-- The width of the app's canvas in pixels. Changing this value has no effect. To
70
	-- set this for real, edit conf.lua. This may *not* correspond to the overall
71
	-- resolution of the window when in fullscreen mode.
72
73
	-- Property: height
74
	-- The height of the window in pixels. Changing this value has no effect. To
75
	-- set this for real, edit conf.lua. This may *not* correspond to the overall
76
	-- resolution of the window when in fullscreen mode.
77
78
	-- Property: fullscreen
79
	-- Whether the app is currently running in fullscreen mode. Changing this value
80
	-- has no effect. To change this, use the enterFullscreen(), exitFullscreen(), or
81
	-- toggleFullscreen() methods.
82
83
	-- Property: inset
84
	-- The screen coordinates where the app's (0, 0) origin should lie. This is only
85
	-- used by Zoetrope to either letterbox or pillar fullscreen apps, but there may
86
	-- be other uses for it.
87
	inset = { x = 0, y = 0},
88
89
	-- internal property: _sleepTime
90
	-- The amount of time the app intentionally slept for, and should not
91
	-- be counted against elapsed time.
92
93
	-- internal property: _nextFrameTime
94
	-- Set at the start of a frame to be the next time a frame should be rendered,
95
	-- in timestamp format. This is used to properly maintain FPS -- see the example
96
	-- in https://love2d.org/wiki/love.timer.sleep for how this works.
97
98
	new = function (self, obj)
99
		obj = self:extend(obj)
100
101
		-- set icon if possible
102
		
103
		if self.icon then
104
			love.graphics.setIcon(Cached:image(self.icon))
105
		end
106
	
107
		-- view containers
108
109
		obj.meta = obj.meta or Group:new()
110
		obj.view = obj.view or View:new()		
111
		
112
		-- input
113
114
		if love.keyboard then
115
			obj.keys = obj.keys or Keys:new()
116
			love.keyboard.setKeyRepeat(0.4, 0.04)
117
			obj.meta:add(obj.keys)
118
		end
119
120
		if love.mouse then
121
			obj.mouse = obj.mouse or Mouse:new()
122
			obj.meta:add(obj.mouse)
123
		end
124
125
		if love.joystick then
126
			obj.gamepads = {}
127
			the.gamepads = obj.gamepads
128
		end
129
130
		if obj.numGamepads and obj.numGamepads > 0 then
131
			for i = 1, obj.numGamepads do
132
				obj.gamepads[i] = Gamepad:new{ number = i }
133
				obj.meta:add(obj.gamepads[i])
134
			end
135
		end
136
137
		-- screen dimensions and state
138
139
		obj.width, obj.height, obj.fullscreen = love.graphics.getMode()
140
141
		-- housekeeping
142
		
143
		the.app = obj
144
		if obj.onNew then obj:onNew() end
145
		return obj
146
	end,
147
148
	-- Method: run
149
	-- Starts the app running. Nothing will occur until this call.
150
	--
151
	-- Arguments:
152
	-- 		none
153
	-- 
154
	-- Returns:
155
	--		nothing
156
157
	run = function (self)
158
		math.randomseed(os.time())
159
160
		-- sync the.view
161
162
		the.view = self.view
163
164
		-- attach debug console
165
166
		if DEBUG then
167
			self.console = DebugConsole:new()
168
			self.meta:add(self.console)
169
		end
170
171
		-- set up callbacks
172
		
173
		love.graphics.setCaption(self.name)
174
		love.update = function (elapsed) self:update(elapsed) end
175
		love.draw = function() self:draw() end
176
		love.focus = function (value) self:onFocus(value) end	
177
178
		if self.onRun then self:onRun() end
179
		self._nextFrameTime = love.timer.getMicroTime()
180
	end,
181
	
182
	-- Method: quit
183
	-- Quits the application immediately.
184
	--
185
	-- Arguments:
186
	--		none
187
	--
188
	-- Returns:
189
	--		nothing
190
191
	quit = function (self)
192
		love.event.quit()
193
	end,
194
195
	-- Method: useSysCursor
196
	-- Shows or hides the system mouse cursor.
197
	--
198
	-- Arguments:
199
	--		value - show the cursor?
200
	--
201
	-- Returns:
202
	--		nothing
203
	
204
	useSysCursor = function (self, value)
205
		if STRICT then
206
			assert(value == true or value == false,
207
				   'tried to set system cursor visibility to ' .. type(value))
208
		end
209
210
		love.mouse.setVisible(value)
211
	end,
212
213
	-- Method: enterFullscreen
214
	-- Enters fullscreen mode. If the app is already in fullscreen, this has no effect.
215
	-- This tries to use the highest resolution that will not result in distortion, and
216
	-- adjust the app's offset property to accomodate this.
217
	--
218
	-- Arguments:
219
	--		hint - whether to try to letterbox (vertical black bars) or pillar the app
220
	--			   (horizontal black bars). You don't need to specify this; the method
221
	--			   will try to infer based on the aspect ratio of your app.
222
	--
223
	-- Returns:
224
	--		boolean whether this succeeded
225
226
	enterFullscreen = function (self, hint)
227
		if STRICT then
228
			assert(not self.fullscreen, 'asked to enter fullscreen when already in fullscreen')
229
		end
230
231
		local modes = love.graphics.getModes()
232
233
		if not hint then
234
			if self.width * 9 == self.height * 16 then
235
				hint = 'letterbox'
236
			elseif self.width * 3 == self.height * 4 then
237
				hint = 'pillar'
238
			end
239
		end
240
241
		-- find the mode with the highest screen area that
242
		-- matches either width or height, according to our hint
243
244
		local bestMode = { area = 0 }
245
246
		for _, mode in pairs(modes) do
247
			mode.area = mode.width * mode.height
248
249
			if (mode.area > bestMode.area) and 
250
			   ((hint == 'letterbox' and mode.width == self.width) or
251
			    (hint == 'pillar' and mode.height == self.height)) then
252
					bestMode = mode
253
			end
254
		end
255
256
		-- if we found a match, switch to it
257
258
		if bestMode.width then
259
			love.graphics.setMode(bestMode.width, bestMode.height, true)
260
			self.fullscreen = true
261
262
			-- and adjust inset and scissor
263
264
			self.inset.x = math.floor((bestMode.width - self.width) / 2)
265
			self.inset.y = math.floor((bestMode.height - self.height) / 2)
266
			love.graphics.setScissor(self.inset.x, self.inset.y, self.width, self.height)
267
268
			if self.onEnterFullscreen then self:onEnterFullscreen() end
269
		end
270
271
		return self.fullscreen
272
	end,
273
274
	-- Method: exitFullscreen
275
	-- Exits fullscreen mode. If the app is already windowed, this has no effect.
276
	--
277
	-- Arguments:
278
	--		none
279
	--
280
	-- Returns:
281
	--		nothing
282
283
	exitFullscreen = function (self)
284
		if STRICT then
285
			assert(self.fullscreen, 'asked to exit fullscreen when already out of fullscreen')
286
		end
287
	
288
		love.graphics.setMode(self.width, self.height, false)
289
		love.graphics.setScissor(0, 0, self.width, self.height)
290
		self.fullscreen = false
291
		self.inset.x = 0
292
		self.inset.y = 0
293
		if self.onExitFullscreen then self:onExitFullscreen() end
294
	end,
295
296
	-- Method: toggleFullscreen
297
	-- Toggles between windowed and fullscreen mode.
298
	--
299
	-- Arguments:
300
	--		none
301
	--
302
	-- Returns:
303
	--		nothing
304
305
	toggleFullscreen = function (self)
306
		if self.fullscreen then
307
			self:exitFullscreen()
308
		else
309
			self:enterFullscreen()
310
		end
311
	end,
312
313
	-- Method: saveScreenshot
314
	-- Saves a snapshot of the current frame to disk.
315
	--
316
	-- Arguments:
317
	--		filename - filename to save as, image format is implied by suffix.
318
	--				   This is forced inside the app's data directory,
319
	--				   see https://love2d.org/wiki/love.filesystem for details.
320
	--
321
	-- Returns:
322
	--		nothing
323
324
	saveScreenshot = function (self, filename)
325
		if not filename then
326
			error('asked to save screenshot to a nil filename')
327
		end
328
329
		local screenshot = love.graphics.newScreenshot()
330
		screenshot:encode(filename)
331
	end,
332
333
	-- Method: add
334
	-- A shortcut for adding a sprite to the app's view.
335
	--
336
	-- Arguments:
337
	--		sprite - sprite to add
338
	-- 
339
	-- Returns:
340
	--		nothing
341
342
	add = function (self, sprite)
343
		self.view:add(sprite)
344
	end,
345
346
	-- Method: remove
347
	-- A shortcut for removing a sprite from the app's view.
348
	--
349
	-- Arguments:
350
	--		sprite - sprite to remove
351
	--
352
	-- Returns: nothing
353
354
	remove = function (self, sprite)
355
		self.view:remove(sprite)
356
	end,
357
358
	update = function (self, elapsed)
359
		-- set the next frame time right at the start
360
		self._nextFrameTime = self._nextFrameTime + 1 / self.fps
361
362
		elapsed = elapsed - (self._sleepTime or 0)
363
364
		local realElapsed = elapsed
365
		elapsed = elapsed * self.timeScale
366
367
		-- sync the.view with our current view
368
		
369
		local view = self.view
370
		if the.view ~= view then the.view = view end
371
372
		-- if we are not active at all, sleep for a half-second
373
374
		if not self.active then
375
			love.timer.sleep(0.5)
376
			self._sleepTime = 0.5
377
			return
378
		end
379
380
		self._sleepTime = 0
381
382
		-- update everyone
383
		-- all update events bubble up from child to parent
384
		-- (we consider the meta view a little 
385
386
		view:startFrame(elapsed)
387
		self.meta:startFrame(elapsed)
388
		if self.onStartFrame then self:onStartFrame(elapsed) end
389
		
390
		view:update(elapsed)	
391
		self.meta:update(elapsed)
392
		if self.onUpdate then self:onUpdate(elapsed) end
393
394
		view:endFrame(elapsed)
395
		self.meta:endFrame(elapsed)
396
		if self.onEndFrame then self:onEndFrame(elapsed) end
397
	end,
398
	
399
	draw = function (self)
400
		local inset = self.inset.x ~= 0 or self.inset.y ~= 0
401
402
		if inset then love.graphics.translate(self.inset.x, self.inset.y) end
403
		self.view:draw()
404
		self.meta:draw()
405
		if inset then love.graphics.translate(0, 0) end
406
407
		-- sleep off any unneeded time to keep up at our FPS
408
409
		local now = love.timer.getMicroTime()
410
411
		if self._nextFrameTime < now then
412
			self._nextFrameTime = now
413
		else
414
			love.timer.sleep(self._nextFrameTime - now)
415
		end
416
	end,
417
418
	onFocus = function (self, value)
419
		if self.deactivateOnBlur then
420
			self.active = value
421
422
			if value then
423
				love.audio.resume()
424
			else
425
				love.audio.pause()
426
			end
427
		end
428
	end,
429
430
	__tostring = function (self)
431
		local result = 'App ('
432
433
		if self.active then
434
			result = result .. 'active'
435
		else
436
			result = result .. 'inactive'
437
		end
438
439
		return result .. ', ' .. self.fps .. ' fps, ' .. self.view:count(true) .. ' sprites)'
440
	end
441
}