"""Multi-line text rendering."""
import pygame as pg
from .conf import conf
from .util import normalise_colour
#: Default values for text rendering options. Value::
#:
#: {
#: 'colour': '000',
#: 'shadow': None,
#: 'width': None,
#: 'just': 0,
#: 'minimise': False,
#: 'line_spacing': 0,
#: 'aa': True,
#: 'bg': None,
#: 'pad': (0, 0, 0, 0),
#: 'wrap': 'char'
#: }
# be sure to update res._mk_text_keys if these change
option_defaults = {
'text_size': 12,
'colour': '000',
'shadow': None,
'width': None,
'just': 0,
'minimise': False,
'line_spacing': 0,
'aa': True,
'bg': (0, 0, 0, 0),
'pad': (0, 0, 0, 0),
'wrap': 'char'
}
[docs]class TextRenderer (object):
"""Render text to a surface.
TextRenderer(font, options={}, pool=conf.DEFAULT_RESOURCE_POOL,
res_mgr=conf.GAME.resources)
:arg font: font filename to use, under :data:`conf.FONT_DIR`.
:arg options: dict giving rendering parameters. These act as default values in
the same argument to :meth:`render`. All options can be
retrieved as properties of this instance (and all are guaranteed
to exist), but cannot be changed.
:arg pool: :class:`ResourceManager <engine.res.ResourceManager>` resource pool
name to cache any loaded Pygame fonts in.
:arg res_mgr: :class:`ResourceManager <engine.res.ResourceManager>` instance to
use to load any Pygame fonts.
"""
def __init__ (self, font, options={}, pool=conf.DEFAULT_RESOURCE_POOL,
res_mgr=None):
self._font = font
self._defaults = option_defaults.copy()
self._defaults.update(options)
self.normalise_options(self._defaults)
self._resource_pool = pool
self._resource_manager = res_mgr
def __eq__ (self, other):
# equal if we would render the exact same thing
if isinstance(other, TextRenderer):
return (self._font == other._font and
self._defaults == other._defaults)
else:
return False
def __getattr__ (self, attr):
if attr in option_defaults:
return self._defaults.get(attr, option_defaults[attr])
else:
return object.__getattribute__(self, attr)
def _get_font (self, opts):
# load the font required for the given (normalised) options
if self._resource_manager is None:
resources = conf.GAME.resources
else:
resources = self._resource_manager
return resources.font(self._font, opts['text_size'],
pool=self._resource_pool)
[docs] def render (self, text, options={}, **kwargs):
"""Render text to a surface.
render(text, options={}, **kwargs) -> (surface, num_lines)
:arg text: text to render; may contain line breaks to display separate lines.
:arg options: dict giving rendering parameters; those omitted default to the
value given in the ``options`` argument to the constructor, and
then the values in :data:`option_defaults`.
:arg kwargs: options can also be passed as keyword arguments, with the option's
name as the argument's name. If an option is given both in
``options`` and as a keyword argument, the keyword argument takes
precedence.
Options available:
:arg text_size: text size, in points.
:arg colour: text colour, as taken by
:func:`util.normalise_colour <engine.util.normalise_colour>`.
Alpha seems to be unsupported by Pygame.
:arg shadow: to draw a drop-shadow: ``(colour, offset)`` tuple, where
``offset`` is ``(x, y)``.
:arg width: maximum width of returned surface (also see ``wrap``). This
excludes padding (``pad``).
:arg just: if the text has multiple lines, justify: ``0`` = left, ``1`` =
centre, ``2`` = right.
:arg minimise: if width is set, treat it as a minimum instead of absolute width
(that is, shrink the surface after, if possible).
:arg line_spacing: space between lines, in pixels.
:arg aa: whether to anti-alias the text.
:arg bg: background colour.
:arg pad: ``(left, top, right, bottom)`` padding in pixels. Can also be one
number for all sides or ``(left_and_right, top_and_bottom)``. This
treats shadow as part of the text.
:arg wrap:
text wrapping mode (only used if ``width`` is given); one of:
- ``'char'`` (default): wrap words and wrap within words if necessary.
- ``'word'``: wrap words only; raises ``ValueError`` if any words won't
fit on a single line.
- ``'none'``: don't wrap: if ``width`` is given, allow text to fall off
the end of the surface.
:return: ``surface`` is the ``pygame.Surface`` containing the rendered text and
``num_lines`` is the final number of lines of text.
"""
opts = self.mk_options(options, **kwargs)
self.normalise_options(opts)
font = self._get_font(opts)
colour = normalise_colour(opts['colour'])
if opts['shadow'] is None:
shadow_colour = None
offset = (0, 0)
else:
shadow_colour, offset = opts['shadow']
just = opts['just']
minimise = opts['minimise']
if opts['width'] is None:
minimise = True
line_spacing = opts['line_spacing']
aa = opts['aa']
bg = opts['bg']
pad = opts['pad']
lines, text_size, sfc_size = self.get_info(text, opts)
width = text_size[0]
opaque = len(bg) == 3 or bg[3] == 255
# simple case: just one line and want to minimise width and no shadow
# or padding and bg is opaque (Pygame seems not to do alpha bg)
if (len(lines) == 1 and minimise and pad == (0, 0, 0, 0) and
shadow_colour is None and opaque):
sfc = font.render(lines[0], True, colour, bg)
return (sfc, 1)
# else create surface to blit all the lines to
sfc = pg.Surface(sfc_size)
if not opaque:
sfc = sfc.convert_alpha()
sfc.fill(bg)
# render and blit text
line_height = font.get_height()
todo = []
if shadow_colour is not None:
todo.append((shadow_colour, offset))
todo.append((colour, (0, 0)))
n_lines = 0
for colour, o in todo:
o = (o[0] + pad[0], o[1] + pad[1])
h = 0
for line in lines:
if line:
n_lines += 1
s = font.render(line, aa, colour)
if just == 2:
sfc.blit(s, (width - s.get_width() + o[0], h + o[1]))
elif just == 1:
sfc.blit(s, ((width - s.get_width()) // 2 + o[0],
h + o[1]))
else:
sfc.blit(s, (o[0], h + o[1]))
h += line_height + line_spacing
return (sfc, n_lines)
[docs] def get_info (self, text, options={}, **kwargs):
"""Get results for render arguments without actually rendering.
get_info(text, options={}, **kwargs) -> (lines, text_size, sfc_size)
Arguments are as taken by :meth:`render`.
:return:
- ``lines``: a list of string lines the text would be split into.
- ``text_size``: the resulting ``(width, height)`` size of the text within
the surface that would be returned, excluding any shadow.
- ``sfc_size``: the resulting size of the surface.
Like :meth:`render`, raises ``ValueError`` if wrapping fails.
"""
opts = self.mk_options(options, **kwargs)
self.normalise_options(opts)
font = self._get_font(opts)
offset = (0, 0) if opts['shadow'] is None else opts['shadow'][1]
width = opts['width']
wrap = opts['wrap']
pad = opts['pad']
# split into lines
lines = text.splitlines()
if width is not None:
text = lines
lines = []
for line in text:
if wrap != 'none' and font.size(line)[0] > width:
# wrap
words = line.split(' ')
# check if any words won't fit
# can't use for as we'll change the list during iteration
i = 0
while i < len(words):
word = words[i]
if font.size(word)[0] > width:
if wrap == 'word':
raise ValueError('\'{0}\' doesn\'t fit on one '
'line'.format(word))
else: # wrap == 'char'
for j in xrange(len(word) - 1, -1, -1):
if font.size(word[:j])[0] <= width:
break
else: # can't be an empty string
j = 1
words[i] = word[:j]
remain = word[j:]
if remain:
words.insert(i + 1, remain)
i += 1
# build line
build = ''
for word in words:
temp = build + ' ' if build else build
temp += word
if font.size(temp)[0] < width:
build = temp
else:
lines.append(build)
build = word
lines.append(build)
else:
lines.append(line)
if width is None or opts['minimise']:
width = max(font.size(line)[0] for line in lines)
# compute sizes
line_height = font.get_height()
n = len(lines)
height = max(n * line_height + (n - 1) * opts['line_spacing'], 0)
sfc_size = (width + abs(offset[0]) + pad[0] + pad[2],
height + abs(offset[1]) + pad[1] + pad[3])
return (lines, (width, height), sfc_size)
[docs] def mk_options (self, options={}, **kwargs):
"""Generate a full set of rendering options given an options dict.
mk_options(options={}, **kwargs) -> new_options
Arguments are as taken by :meth:`render`.
:return: The completed dict of rendering options, with defaults filled in.
"""
opts = self._defaults.copy()
opts.update(options, **kwargs)
return opts
[docs] def normalise_options (self, options={}):
"""Normalise a (possibly incomplete) renderer options dict, in-place.
Arguments are as taken by :meth:`render`.
This involves making every option hashable and putting it in a standard format.
"""
o = options
if 'text_size' in o:
o['text_size'] = int(o['text_size'])
if 'colour' in o:
o['colour'] = normalise_colour(o['colour'])
shadow = o.get('shadow')
if shadow is not None:
shadow = (normalise_colour(shadow[0]), tuple(shadow[1][:2]))
o['shadow'] = shadow
if o.get('width') is not None:
o['width'] = int(o['width'])
if 'just' in o:
o['just'] = int(o['just'])
if 'minimise' in o:
o['minimise'] = bool(o['minimise'])
if 'line_spacing' in o:
o['line_spacing'] = int(o['line_spacing'])
if 'aa' in o:
o['aa'] = bool(o['aa'])
if 'bg' in o:
o['bg'] = normalise_colour(o['bg'])
if 'pad' in o:
pad = o['pad']
if isinstance(pad, int):
pad = (pad, pad, pad, pad)
elif len(pad) == 2:
pad = tuple(pad)
pad = pad + pad
else:
pad = tuple(pad)
o['pad'] = pad
if 'wrap' in o and o['wrap'] not in ('char', 'word', 'none'):
raise ValueError('unknown wrap mode: \'{0}\''.format(o['wrap']))