Repository navigation
Expand file tree
/
Copy pathpysdl2.py
More file actions
450 lines (421 loc) · 16.5 KB
/
Copy pathpysdl2.py
File metadata and controls
450 lines (421 loc) · 16.5 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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
"""
Example of embedding CEF browser using PySDL2 library.
This example is incomplete, see "Missing functionality" section
further down. Pull requests for the missing functionality are welcome.
Requires PySDL2 and SDL2 libraries.
Tested configurations:
- Windows 7: SDL 2.0.7 and PySDL2 0.9.6
- Fedora 26: SDL2 2.0.7 with PySDL2 0.9.6
- Ubuntu 14.04: SDL2 with PySDL2 0.9.6
Install instructions:
1. Install SDL libraries for your OS, e.g:
- Windows: Download SDL2.dll from http://www.libsdl.org/download-2.0.php
and put SDL2.dll in C:\Python27\ (where you've installed Python)
- Fedora: sudo dnf install SDL2 SDL2_ttf SDL2_image SDL2_gfx SDL2_mixer
- Ubuntu: sudo apt-get install libsdl2-dev
2. Install PySDL2 using pip package manager:
pip install PySDL2
Missing functionality:
- Performance is still not perfect, see Issue #324 for further details
- Keyboard modifiers that are not yet handled in this example:
ctrl, marking text inputs with the shift key.
- Mouse dragging
- Window size is fixed, cannot be resized
GUI controls:
Due to SDL2's lack of GUI widgets there are no GUI controls
for the user. However, as an exercise this example could
be extended by create some simple SDL2 widgets. An example of
widgets made using PySDL2 can be found as part of the Pi
Entertainment System at:
https://github.com/neilmunday/pes/blob/master/lib/pes/ui.py
"""
import argparse
import logging
import sys
def die(msg):
"""
Helper function to exit application on failed imports etc.
"""
sys.stderr.write("%s\n" % msg)
sys.exit(1)
try:
# noinspection PyUnresolvedReferences
from cefpython3 import cefpython as cef
except ImportError:
die("ERROR: cefpython3 package not found\nTo install type: `pip install cefpython3`")
try:
# noinspection PyUnresolvedReferences
import sdl2
# noinspection PyUnresolvedReferences
import sdl2.ext
except ImportError:
die("ERROR: SDL2 package not found\nTo install type: `pip install PySDL2`")
try:
# noinspection PyUnresolvedReferences
from PIL import Image
except ImportError:
die("ERROR: PIL package not found\nTo install type: pip install Pillow")
def main():
parser = argparse.ArgumentParser(description='PySDL2 / cefpython example', add_help=True)
parser.add_argument('-v', '--verbose', help='Turn on debug info', dest='verbose', action='store_true')
args = parser.parse_args()
logLevel = logging.INFO
if args.verbose:
logLevel = logging.DEBUG
logging.basicConfig(format='[%(filename)s %(levelname)s]: %(message)s', level=logLevel)
# The following variables control the dimensions of the window
# and browser display area
width = 800
height = 600
# headerHeight is useful for leaving space for controls
# at the top of the window (future implementation?)
headerHeight = 0
browserHeight = height - headerHeight
browserWidth = width
# Mouse wheel fudge to enhance scrolling
scrollEnhance = 40
# desired frame rate
frameRate = 100
# Initialise CEF for offscreen rendering
sys.excepthook = cef.ExceptHook
switches = {
# Tweaking OSR performance by setting the same Chromium flags
# as in upstream cefclient (Issue #240).
"disable-surfaces": "",
"disable-gpu": "",
"disable-gpu-compositing": "",
"enable-begin-frame-scheduling": "",
}
browser_settings = {
# Tweaking OSR performance (Issue #240)
"windowless_frame_rate": frameRate
}
cef.Initialize(settings={"windowless_rendering_enabled": True},
switches=switches)
logging.debug("cef initialised")
window_info = cef.WindowInfo()
window_info.SetAsOffscreen(0)
# Initialise SDL2 for video (add other init constants if you
# require other SDL2 functionality e.g. mixer,
# TTF, joystick etc.
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
logging.debug("SDL2 initialised")
# Create the window
window = sdl2.video.SDL_CreateWindow(
'cefpython3 SDL2 Demo',
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
width,
height,
0
)
# Define default background colour (black in this case)
backgroundColour = sdl2.SDL_Color(0, 0, 0)
# Create the renderer using hardware acceleration
renderer = sdl2.SDL_CreateRenderer(window, -1,
sdl2.render.SDL_RENDERER_ACCELERATED)
# Set-up the RenderHandler, passing in the SDL2 renderer
renderHandler = RenderHandler(renderer, width, height - headerHeight)
# Create the browser instance
browser = cef.CreateBrowserSync(window_info,
url="https://www.google.com/",
settings=browser_settings)
browser.SetClientHandler(LoadHandler())
browser.SetClientHandler(renderHandler)
# Must call WasResized at least once to let know CEF that
# viewport size is available and that OnPaint may be called.
browser.SendFocusEvent(True)
browser.WasResized()
# Begin the main rendering loop
running = True
# FPS debug variables
if logLevel == logging.DEBUG:
frames = 0
lastFrameTick = sdl2.timer.SDL_GetTicks()
logging.debug("beginning rendering loop")
while running:
# record when we started drawing this frame
startTime = sdl2.timer.SDL_GetTicks()
# Convert SDL2 events into CEF events (where appropriate)
events = sdl2.ext.get_events()
for event in events:
if (event.type == sdl2.SDL_QUIT
or (event.type == sdl2.SDL_KEYDOWN
and event.key.keysym.sym == sdl2.SDLK_ESCAPE)):
running = False
break
if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
False,
1
)
elif event.type == sdl2.SDL_MOUSEBUTTONUP:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
True,
1
)
elif event.type == sdl2.SDL_MOUSEMOTION:
if event.motion.y > headerHeight:
# Mouse move triggered in browser region
browser.SendMouseMoveEvent(event.motion.x,
event.motion.y - headerHeight,
False)
elif event.type == sdl2.SDL_MOUSEWHEEL:
# Mouse wheel event
x = event.wheel.x
if x < 0:
x -= scrollEnhance
else:
x += scrollEnhance
y = event.wheel.y
if y < 0:
y -= scrollEnhance
else:
y += scrollEnhance
browser.SendMouseWheelEvent(0, 0, x, y)
elif event.type == sdl2.SDL_TEXTINPUT:
# Handle text events to get actual characters typed rather
# than the key pressed.
keycode = ord(event.text.text)
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYDOWN:
# Handle key down events for non-text keys
if event.key.keysym.sym == sdl2.SDLK_RETURN:
keycode = event.key.keysym.sym
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.key.keysym.sym in [
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = get_key_code(event.key.keysym.sym)
if keycode is not None:
key_event = {
"type": cef.KEYEVENT_RAWKEYDOWN,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYUP:
# Handle key up events for non-text keys
if event.key.keysym.sym in [
sdl2.SDLK_RETURN,
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = get_key_code(event.key.keysym.sym)
if keycode is not None:
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
# Clear the renderer
sdl2.SDL_SetRenderDrawColor(
renderer,
backgroundColour.r,
backgroundColour.g,
backgroundColour.b,
255
)
sdl2.SDL_RenderClear(renderer)
# Tell CEF to update which will trigger the OnPaint
# method of the RenderHandler instance
cef.MessageLoopWork()
# Update display
sdl2.SDL_RenderCopy(
renderer,
renderHandler.texture,
None,
sdl2.SDL_Rect(0, headerHeight, browserWidth, browserHeight)
)
sdl2.SDL_RenderPresent(renderer)
# FPS debug code
if logLevel == logging.DEBUG:
frames += 1
currentTick = sdl2.timer.SDL_GetTicks()
if currentTick - lastFrameTick > 1000:
lastFrameTick = sdl2.timer.SDL_GetTicks()
logging.debug("FPS %d" % (frames / (currentTick / 1000.0)))
# regulate frame rate
if sdl2.timer.SDL_GetTicks() - startTime < 1000.0 / frameRate:
sdl2.timer.SDL_Delay((1000 / frameRate) - (sdl2.timer.SDL_GetTicks() - startTime))
# User exited
exit_app()
def get_key_code(key):
"""Helper function to convert SDL2 key codes to cef ones"""
key_map = {
sdl2.SDLK_RETURN: 13,
sdl2.SDLK_DELETE: 46,
sdl2.SDLK_BACKSPACE: 8,
sdl2.SDLK_LEFT: 37,
sdl2.SDLK_RIGHT: 39,
sdl2.SDLK_UP: 38,
sdl2.SDLK_DOWN: 40,
sdl2.SDLK_HOME: 36,
sdl2.SDLK_END: 35,
}
if key in key_map:
return key_map[key]
# Key not mapped, raise exception
logging.error("Keyboard mapping incomplete:"
" unsupported SDL key %d."
" See https://wiki.libsdl.org/SDLKeycodeLookup for mapping."
% key)
return None
class LoadHandler(object):
"""Simple handler for loading URLs."""
def OnLoadingStateChange(self, is_loading, **_):
if not is_loading:
logging.info("Page loading complete")
def OnLoadError(self, frame, failed_url, **_):
if not frame.IsMain():
return
logging.error("Failed to load %s" % failed_url)
class RenderHandler(object):
"""
Handler for rendering web pages to the
screen via SDL2.
The object's texture property is exposed
to allow the main rendering loop to access
the SDL2 texture.
"""
def __init__(self, renderer, width, height):
self.__width = width
self.__height = height
self.__renderer = renderer
self.texture = None
def GetViewRect(self, rect_out, **_):
rect_out.extend([0, 0, self.__width, self.__height])
return True
def OnPaint(self, element_type, paint_buffer, **_):
"""
Using the pixel data from CEF's offscreen rendering
the data is converted by PIL into a SDL2 surface
which can then be rendered as a SDL2 texture.
"""
if element_type == cef.PET_VIEW:
image = Image.frombuffer(
'RGBA',
(self.__width, self.__height),
paint_buffer.GetString(mode="rgba", origin="top-left"),
'raw',
'BGRA'
)
# Following PIL to SDL2 surface code from pysdl2 source.
mode = image.mode
rmask = gmask = bmask = amask = 0
depth = None
pitch = None
if mode == "RGB":
# 3x8-bit, 24bpp
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x0000FF
gmask = 0x00FF00
bmask = 0xFF0000
else:
rmask = 0xFF0000
gmask = 0x00FF00
bmask = 0x0000FF
depth = 24
pitch = self.__width * 3
elif mode in ("RGBA", "RGBX"):
# RGBX: 4x8-bit, no alpha
# RGBA: 4x8-bit, alpha
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x00000000
gmask = 0x0000FF00
bmask = 0x00FF0000
if mode == "RGBA":
amask = 0xFF000000
else:
rmask = 0xFF000000
gmask = 0x00FF0000
bmask = 0x0000FF00
if mode == "RGBA":
amask = 0x000000FF
depth = 32
pitch = self.__width * 4
else:
logging.error("ERROR: Unsupported mode: %s" % mode)
exit_app()
pxbuf = image.tobytes()
# Create surface
surface = sdl2.SDL_CreateRGBSurfaceFrom(
pxbuf,
self.__width,
self.__height,
depth,
pitch,
rmask,
gmask,
bmask,
amask
)
if self.texture:
# free memory used by previous texture
sdl2.SDL_DestroyTexture(self.texture)
# Create texture
self.texture = sdl2.SDL_CreateTextureFromSurface(self.__renderer,
surface)
# Free the surface
sdl2.SDL_FreeSurface(surface)
else:
logging.warning("Unsupport element_type in OnPaint")
def exit_app():
"""Tidy up SDL2 and CEF before exiting."""
sdl2.SDL_Quit()
cef.Shutdown()
logging.info("Exited gracefully")
if __name__ == "__main__":
main()