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
|
#!/usr/bin/env python
'''
'''
from OpenGL.GL import *
import pyglyph
import pyglyph.font
from functools import reduce
__docformat__ = 'restructuredtext'
__version__ = '$Id: layout.py,v 1.4 2006/05/25 14:14:40 alex Exp $'
class Align:
"""Enumeration of alignments."""
left, right, top, bottom, center = range(5)
class Style:
"""A character style.
Currently consists of just a font instance and a color, but could
be marked up later to include things like underlining, super and
subscript, etc."""
def __init__(self, font, color):
"""Create a new character style.
:Parameters:
`font` : pyglyph.font.FontInstance
Font instance for this style
`color` : 3 or 4-tuple of float
Color passed to glColor
"""
self.font_instance = font
self.color = color
def tuple(self):
return (self.font_instance, self.color)
def __cmp__(self, other):
return cmp(self.tuple(), other.tuple())
def __hash__(self):
return hash(self.tuple())
class StyledRun:
"""A sequence of characters with the same character style.
The internal representation is the list of boxes returned by
`pyglyph.font.FontInstance.get_boxes`. The run can be translated
in space (equivalent to ``glTranslate``) and sliced efficiently.
"""
def __init__(self, text, style, boxes=None, advance=None):
"""Construct a styled run of text.
:Parameters:
`text` : str
Text or unicode text to be displayed
`style` : Style
Style to format these characters with
`boxes`
List of boxes obtained from
`pyglyph.font.FontInstance.get_boxes`. If omitted, will
be calculated.
`advance` : number
Horizontal advance for the run. Will be calculated if
omitted.
"""
self.text = text
self.style = style
if not boxes or not advance:
self.boxes, self.advance = style.font_instance.get_boxes(text)
else:
self.boxes = boxes
self.advance = advance
def slice(self, start=0, end=None):
"""Return a new StyledRun which is a slice of this one.
The parameter semantics are the same as a Python built-in slice.
"""
r = StyledRun(self.text[start:end],
self.style,
self.boxes[start:end],
self.boxes[(end or 0)-1][0][2] - self.boxes[start][0][0])
r.translate(-r.boxes[0][0][0], 0)
return r
def translate(self, x, y):
"""Translate this slice by the given deltas."""
self.boxes = [((box[0][0] + x,
box[0][1] + y,
box[0][2] + x,
box[0][3] + y),
box[1]) for box in self.boxes]
def __repr__(self):
return 'StyledRun(%s, %s)' % (self.text, self.style)
class ParagraphMarker:
"""A paragraph style that can be inserted into a layout.
Currently the only paragraph-level attribute is justification, but
future expected attributes are margin, leading, hanging indent,
and so on.
"""
def __init__(self, style, justification=Align.left):
"""Construct a ParagraphMarker with the given style attributes.
:Parameters:
`style` : Style
Style for this paragraph marker (not used).
`justification` : int
Valid justifications are
* `pyglyph.Align.left`
* `pyglyph.Align.right`
* `pyglyph.Align.center`
defaults to `pyglyph.Align.left`.
"""
self.style = style
self.justification = justification
class StyledRunLine:
"""A completed line of laid-out text.
The line consists of a list of `StyledRun`. In the future the width
and height attributes may be used to efficiently cull text not
visible in a viewport. Currently this class is a little redundant
though."""
def __init__(self, runs, width, height):
"""Construct a new StyledRunLine with the given runs."""
self.runs = runs
# TODO width height necessary?
self.width = width
self.height = height
def __repr__(self):
return 'StyledRunLine(%s, %d, %d)' % \
(self.runs, self.width, self.height)
class TextLayout:
"""Automatic line-wrapping and justification of attributed text.
To use this class directly, instantiate it with the width of the
box the text will be flowed into. Then call `layout` with a list
of all `StyledRun` of text. The resultant lines are then available
in the ``lines`` attribute, which is a list of `StyledRunLine`.
For rendering the text, see `OpenGLTextLayout`.
"""
def __init__(self, width=-1):
"""Construct a TextLayout of the given width.
:Parameters:
`width` : int
Width of the layout, in pixels. Text will be wrapped into
this width. If -1, text will not be wrapped and width
will expand to fit text.
The layout can be reused by calling `layout` as many times as
necessary; ``lines`` will be cleared each time.
"""
self.width = width
self.height = 0
def words(self, runs):
"""Find potential breakpoints in a list of runs.
This is a generator method that returns breakpoints continuously
until all have been found. Each return value is a list of
`StyledRun`.
"""
buffer = []
for run in runs:
if isinstance(run, ParagraphMarker):
if buffer:
yield buffer
buffer = []
yield run
else:
idx = run.text.find(' ')
start = 0
while idx != -1:
if start != idx:
buffer.append(run.slice(start, idx))
yield buffer
buffer = []
start = idx + 1
idx = run.text.find(' ', start)
if start < len(run.text):
buffer.append(run.slice(start, None))
if buffer:
yield buffer
def _commit_line(self):
if self.current_line:
self.y -= self.current_line_ascent - self.last_line_descent
x = 0
if self.justification == Align.right:
x = self.width - self.current_line_width
elif self.justification == Align.center:
x = (self.width - self.current_line_width) // 2
for run in self.current_line:
run.translate(x, self.y)
self.lines.append(StyledRunLine(self.current_line,
self.current_line_width, -1))
self.last_line_descent = self.current_line_descent
else:
self.y -= self.current_line_ascent - self.last_line_descent
self.current_line = []
self.current_line_width = 0
self.current_line_ascent = 0
self.current_line_descent = 0
self.spacer_advance = 0
def layout(self, runs):
"""Layout attributed text into the flow width.
:Parameters:
`runs` : list
Each element of the list is either a `StyledRun` or
`ParagraphMarker`.
There is no return value, but the ``lines`` attribute is set to
a list of `StyledRunLine`.
"""
self.height = 0
self.lines = []
self.current_line = []
self.current_line_width = 0
self.current_line_ascent = 0
self.current_line_descent = 0
self.last_line_descent = 0
self.spacer_advance = 0
self.justification = Align.left
self.y = 0
for word in self.words(runs):
if isinstance(word, ParagraphMarker):
self.current_line_ascent = max(self.current_line_ascent,
word.style.font_instance.ascent)
self.current_line_descent = min(self.current_line_descent,
word.style.font_instance.descent)
self._commit_line()
self.justification = word.justification
continue
if self.current_line:
spacer = StyledRun(' ', self.current_line[-1].style)
self.spacer_advance = spacer.advance
self.word_advance = reduce(lambda a,b:a + b.advance, word, 0)
if self.word_advance + \
self.spacer_advance + \
self.current_line_width > self.width and self.width != -1:
self._commit_line()
x = self.current_line_width + self.spacer_advance
for run in word:
run.translate(x, 0)
x += run.advance
self.current_line += word
self.current_line_width += self.word_advance + self.spacer_advance
self.current_line_ascent = max(self.current_line_ascent,
reduce(lambda a,b:max(a,b.style.font_instance.ascent), word, 0))
self.current_line_descent = min(self.current_line_descent,
reduce(lambda a,b:min(a,b.style.font_instance.descent), word, 0))
if self.current_line:
self._commit_line()
self.height = -self.y - self.last_line_descent
if self.width == -1:
self.width = x
def draw(self):
"""Subclasses override this method for implementation-specific
rendering."""
raise NotImplementedError()
class OpenGLTextLayout(TextLayout):
"""Text layout for rendering in OpenGL.
In addition to performing text layout, OpenGL state changes are grouped
together and minimised in order to increase drawing efficiency.
"""
def __init__(self, *args, **kwargs):
"""Construct a OpenGLTextLayout.
See `TextLayout.__init__` for accepted parameters.
"""
TextLayout.__init__(self, *args, **kwargs)
self._contexts = {}
def layout(self, runs):
"""Layout attributed text into the flow width.
This method extends `TextLayout.layout` by finding OpenGL state
changes and sorting on them for rendering efficiency.
"""
TextLayout.layout(self, runs)
self._contexts = {}
runs = reduce(lambda a,b: a + b.runs, self.lines, [])
for run in runs:
if not run.style in self._contexts:
self._contexts[run.style] = []
self._contexts[run.style] += run.boxes
def draw(self, pos=(0,0),
anchor=(Align.left, Align.top)):
"""Draw the layout to the current GL context.
:Parameters:
`pos` : tuple of (int, int)
Position (x, y) to draw the layout
`anchor` : tuple of (int, int)
Alignment of anchor position (x, y), where x is one of:
* pyglyph.Align.left
* pyglyph.Align.center
* pyglyph.Align.right
and y is one of
* pyglyph.Align.top
* pyglyph.Align.center
* pyglyph.Align.bottom.
Defaults to left, top.
The `anchor` and `pos` parameters determine the position of the
layout in x, y coordinates. For example, specifying ``pos = (50,20)``
and ``anchor = (pyglyph.Align.center, pyglyph.Align.center)`` will
center the layout on those coordinates.
This method assumes the context has the necessary drawing
state; see `pyglyph.begin`.
"""
x, y = pos
if anchor[0] == pyglyph.Align.center:
x -= self.width//2
elif anchor[0] == pyglyph.Align.right:
x -= self.width
if anchor[1] == pyglyph.Align.center:
y += self.height//2
elif anchor[1] == pyglyph.Align.bottom:
y += self.height
glTranslatef(x, y, 0)
last_color = None
for style, boxes in self._contexts.items():
if style.color != last_color:
glColor4f(*style.color)
style.font_instance.draw_boxes(boxes)
glTranslatef(-x, -y, 0)
|