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
|
# -*- coding: utf-8 -*-
import pygame
__author__ = "Keul - lucafbb AT gmail.com"
__version__ = "0.1.0"
__description__ = "A PyGame addon for display text over surface with many dimension bounds"
LL_CUT = "cut"
LL_SPLIT = "split"
LL_OVERFLOW = "overflow"
PL_OVERFLOW = "overflow"
PL_CUT = "cut"
class KTextSurfaceWriter(object):
"""Generate a text displayed inside a given pygame.Rect.
You can change/choose font size and color, and can fill the surface part.
"""
def __init__(self, rect, font=None, color=(0,0,255,0), fillcolor=(0,0,0,0), justify_chars=0):
self.rect = rect
if not font:
self.font = pygame.font.Font(None, 30)
else:
self.font = font
self.fillcolor = fillcolor
self.color = color
self._text = "KTextSurfaceWriter - version %s" % __version__
self._resultPage = []
self.justify_chars = justify_chars
self._mustClear = True
self.line_length_criteria = LL_CUT
self.page_length_criteria = PL_OVERFLOW
def _setText(self, text):
self._text = text
self.invalidate()
text = property(lambda self: self._text, _setText, doc="""The text to be displayed""")
def invalidate(self):
"""Manually invalidate the text cache"""
self._resultPage = []
self._mustClear = True
@classmethod
def wordTooLong(cls, word, font, max_length, justify_chars=0):
"""test if a single word is too long to the displayed with the given font.
@word: the word to check
@font: the pygame.Font to use
@max_length: the max length of the word
@justify_chars: an integer that add a number of spaces at the worrd total length.
@return: True if the word will be longer
"""
# BBB: someday this function could became part of some text (non graphical) utility?
return font.size((" "*justify_chars)+word)[0]>max_length
@classmethod
def normalizeTextLength(cls, text_too_long, font, max_length, justify_chars=0, line_length_criteria=LL_CUT):
"""This function take a text too long and split it in a list of smaller text lines.
The final text max length must be less/equals than max_length parameter, using the font passed.
@return: a list of text lines.
"""
# BBB: someday this function could became part of some text (non graphical) utility?
words = [x for x in text_too_long.split(" ")]
words_removed = []
tooLong = True
txt1 = txt2 = ""
while tooLong:
word = words.pop()
if line_length_criteria.lower()==LL_CUT:
# Simple: cut the word and go on
while cls.wordTooLong(word, font, max_length, justify_chars=justify_chars):
word = word[:-1].strip()
elif line_length_criteria.lower()==LL_SPLIT:
# Cut the word, re-insert the remaining part as a new word and start again
if cls.wordTooLong(word, font, max_length, justify_chars=justify_chars):
left_word = word
while cls.wordTooLong(left_word, font, max_length, justify_chars=justify_chars):
left_word = left_word[:-1].strip()
words.extend( [left_word, word[len(left_word):], ] )
continue
elif line_length_criteria.lower()==LL_OVERFLOW:
# Word too long is not changed, so draw outside the defined rect
txt1 = " ".join(words)
if cls.wordTooLong(word, font, max_length, justify_chars=justify_chars):
words_removed.reverse()
txt2 = (" "*justify_chars) + " ".join(words_removed)
if font.size(txt2)[0]<=max_length:
return cls.normalizeTextLength(txt1, font, max_length, justify_chars=justify_chars) + \
[(" "*justify_chars) + word, txt2]
else:
return cls.normalizeTextLength(txt1, font, max_length, justify_chars=justify_chars) + \
[(" "*justify_chars) + word] + \
cls.normalizeTextLength(txt2, font, max_length, justify_chars=justify_chars)
else:
raise ValueError("Invalid line_length_criteria value: %s" % line_length_criteria)
words_removed.append(word)
txt1 = " ".join(words)
if font.size(txt1)[0]<=max_length:
tooLong = False
words_removed.reverse()
txt2 = (" "*justify_chars) + " ".join(words_removed)
if font.size(txt2)[0]<=max_length:
return [txt1, txt2]
else:
return [txt1] + cls.normalizeTextLength(txt2, font, max_length, justify_chars=justify_chars)
def _getPreparedText(self):
"""Prepare text for future rendering.
@return: a list of all lines to be drawn
"""
if self._resultPage:
return self._resultPage
rw = self.rect.width
rh = self.rect.height
text = self.text
resultPage = []
for line in text.split("\n"):
lw, lh = self.font.size(line)
if lw>rw:
newtextlines = self.normalizeTextLength(line,
self.font,
rw,
justify_chars=self.justify_chars,
line_length_criteria=self.line_length_criteria)
else:
newtextlines = [line,]
resultPage.extend(newtextlines)
if self.page_length_criteria==PL_CUT:
resultPage = self._shortDownPage(resultPage)
self._resultPage = resultPage
return resultPage
def _shortDownPage(self, page, start_from_line=0):
"""If the page is too tall with the current font, this method will
remove as many lines as needed to fith the text inside the
constraint rect
@page: the list of lines text
@start_from_line: use this to not keep the page from the beginning
@return: the page parameter without the not needed lines.
"""
ln = len(page)
while ln*self.font.get_height()>self.rect.height:
page.pop()
ln = len(page)
return page
def clear(self, surface, fillcolor=None):
"""Clear the subsurface with the fillcolor choosen"""
if not fillcolor:
fillcolor = self.fillcolor
subs = surface.subsurface(self.rect)
subs.fill(fillcolor)
def draw(self, surface):
"""Draw the text to the surface."""
if self._mustClear:
self.clear(surface)
self._mustClear = False
resultPage = self._getPreparedText()
rect = self.rect
i = 0
for line in resultPage:
ren = self.font.render(line, 1, self.color, self.fillcolor)
surface.blit(ren, (rect.left, rect.top + i*self.font.get_height()))
i+=1
def runTests():
"""Just run the doctest"""
import tests
|