heroes-renaissance/pyglet/text/runlist.py
2008-11-23 21:07:47 +01:00

418 lines
14 KiB
Python

# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of pyglet nor the names of its
# contributors may be used to endorse or promote products
# derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
'''Run list encoding utilities.
:since: pyglet 1.1
'''
__docformat__ = 'restructuredtext'
__version__ = '$Id: $'
class _Run(object):
def __init__(self, value, count):
self.value = value
self.count = count
def __repr__(self):
return 'Run(%r, %d)' % (self.value, self.count)
class RunList(object):
'''List of contiguous runs of values.
A `RunList` is an efficient encoding of a sequence of values. For
example, the sequence ``aaaabbccccc`` is encoded as ``(4, a), (2, b),
(5, c)``. The class provides methods for modifying and querying the
run list without needing to deal with the tricky cases of splitting and
merging the run list entries.
Run lists are used to represent formatted character data in pyglet. A
separate run list is maintained for each style attribute, for example,
bold, italic, font size, and so on. Unless you are overriding the
document interfaces, the only interaction with run lists is via
`RunIterator`.
The length and ranges of a run list always refer to the character
positions in the decoded list. For example, in the above sequence,
``set_run(2, 5, 'x')`` would change the sequence to ``aaxxxbccccc``.
'''
def __init__(self, size, initial):
'''Create a run list of the given size and a default value.
:Parameters:
`size` : int
Number of characters to represent initially.
`initial` : object
The value of all characters in the run list.
'''
self.runs = [_Run(initial, size)]
def insert(self, pos, length):
'''Insert characters into the run list.
The inserted characters will take on the value immediately preceding
the insertion point (or the value of the first character, if `pos` is
0).
:Parameters:
`pos` : int
Insertion index
`length` : int
Number of characters to insert.
'''
i = 0
for run in self.runs:
if i <= pos <= i + run.count:
run.count += length
i += run.count
def delete(self, start, end):
'''Remove characters from the run list.
:Parameters:
`start` : int
Starting index to remove from.
`end` : int
End index, exclusive.
'''
i = 0
for run in self.runs:
if end - start == 0:
break
if i <= start <= i + run.count:
trim = min(end - start, i + run.count - start)
run.count -= trim
end -= trim
i += run.count
self.runs = [r for r in self.runs if r.count > 0]
# Don't leave an empty list
if not self.runs:
self.runs = [_Run(run.value, 0)]
def set_run(self, start, end, value):
'''Set the value of a range of characters.
:Parameters:
`start` : int
Start index of range.
`end` : int
End of range, exclusive.
`value` : object
Value to set over the range.
'''
if end - start <= 0:
return
# Find runs that need to be split
i = 0
start_i = None
start_trim = 0
end_i = None
end_trim = 0
for run_i, run in enumerate(self.runs):
count = run.count
if i < start < i + count:
start_i = run_i
start_trim = start - i
if i < end < i + count:
end_i = run_i
end_trim = end - i
i += count
# Split runs
if start_i is not None:
run = self.runs[start_i]
self.runs.insert(start_i, _Run(run.value, start_trim))
run.count -= start_trim
if end_i is not None:
if end_i == start_i:
end_trim -= start_trim
end_i += 1
if end_i is not None:
run = self.runs[end_i]
self.runs.insert(end_i, _Run(run.value, end_trim))
run.count -= end_trim
# Set new value on runs
i = 0
for run in self.runs:
if start <= i and i + run.count <= end:
run.value = value
i += run.count
# Merge adjacent runs
last_run = self.runs[0]
for run in self.runs[1:]:
if run.value == last_run.value:
run.count += last_run.count
last_run.count = 0
last_run = run
# Delete collapsed runs
self.runs = [r for r in self.runs if r.count > 0]
def __iter__(self):
i = 0
for run in self.runs:
yield i, i + run.count, run.value
i += run.count
def get_run_iterator(self):
'''Get an extended iterator over the run list.
:rtype: `RunIterator`
'''
return RunIterator(self)
def __getitem__(self, index):
'''Get the value at a character position.
:Parameters:
`index` : int
Index of character. Must be within range and non-negative.
:rtype: object
'''
i = 0
for run in self.runs:
if i <= index < i + run.count:
return run.value
i += run.count
# Append insertion point
if index == i:
return self.runs[-1].value
assert False, 'Index not in range'
def __repr__(self):
return str(list(self))
class AbstractRunIterator(object):
'''Range iteration over `RunList`.
`AbstractRunIterator` objects allow any monotonically non-decreasing
access of the iteration, including repeated iteration over the same index.
Use the ``[index]`` operator to get the value at a particular index within
the document. For example::
run_iter = iter(run_list)
value = run_iter[0]
value = run_iter[0] # non-decreasing access is OK
value = run_iter[15]
value = run_iter[17]
value = run_iter[16] # this is illegal, the index decreased.
Using `AbstractRunIterator` to access increasing indices of the value runs
is more efficient than calling `RunList.__getitem__` repeatedly.
You can also iterate over monotonically non-decreasing ranges over the
iteration. For example::
run_iter = iter(run_list)
for start, end, value in run_iter.ranges(0, 20):
pass
for start, end, value in run_iter.ranges(25, 30):
pass
for start, end, value in run_iter.ranges(30, 40):
pass
Both start and end indices of the slice are required and must be positive.
'''
def __getitem__(self, index):
'''Get the value at a given index.
See the class documentation for examples of valid usage.
:Parameters:
`index` : int
Document position to query.
:rtype: object
'''
def ranges(self, start, end):
'''Iterate over a subrange of the run list.
See the class documentation for examples of valid usage.
:Parameters:
`start` : int
Start index to iterate from.
`end` : int
End index, exclusive.
:rtype: iterator
:return: Iterator over (start, end, value) tuples.
'''
class RunIterator(AbstractRunIterator):
def __init__(self, run_list):
self.next = iter(run_list).next
self.start, self.end, self.value = self.next()
def __getitem__(self, index):
while index >= self.end:
self.start, self.end, self.value = self.next()
return self.value
def ranges(self, start, end):
while start >= self.end:
self.start, self.end, self.value = self.next()
yield start, min(self.end, end), self.value
while end > self.end:
self.start, self.end, self.value = self.next()
yield self.start, min(self.end, end), self.value
class OverriddenRunIterator(AbstractRunIterator):
'''Iterator over a `RunIterator`, with a value temporarily replacing
a given range.
'''
def __init__(self, base_iterator, start, end, value):
'''Create a derived iterator.
:Parameters:
`start` : int
Start of range to override
`end` : int
End of range to override, exclusive
`value` : object
Value to replace over the range
'''
self.iter = base_iterator
self.override_start = start
self.override_end = end
self.override_value = value
def ranges(self, start, end):
if end <= self.override_start or start >= self.override_end:
# No overlap
for r in self.iter.ranges(start, end):
yield r
else:
# Overlap: before, override, after
if start < self.override_start < end:
for r in self.iter.ranges(start, self.override_start):
yield r
yield (max(self.override_start, start),
min(self.override_end, end),
self.override_value)
if start < self.override_end < end:
for r in self.iter.ranges(self.override_end, end):
yield r
def __getitem__(self, index):
if self.override_start <= index < self.override_end:
return self.override_value
else:
return self.iter[index]
class FilteredRunIterator(AbstractRunIterator):
'''Iterate over an `AbstractRunIterator` with filtered values replaced
by a default value.
'''
def __init__(self, base_iterator, filter, default):
'''Create a filtered run iterator.
:Parameters:
`base_iterator` : `AbstractRunIterator`
Source of runs.
`filter` : ``lambda object: bool``
Function taking a value as parameter, and returning ``True``
if the value is acceptable, and ``False`` if the default value
should be substituted.
`default` : object
Default value to replace filtered values.
'''
self.iter = base_iterator
self.filter = filter
self.default = default
def ranges(self, start, end):
for start, end, value in self.iter.ranges(start, end):
if self.filter(value):
yield start, end, value
else:
yield start, end, self.default
def __getitem__(self, index):
value = self.iter[index]
if self.filter(value):
return value
return self.default
class ZipRunIterator(AbstractRunIterator):
'''Iterate over multiple run iterators concurrently.'''
def __init__(self, range_iterators):
self.range_iterators = range_iterators
def ranges(self, start, end):
iterators = [i.ranges(start, end) for i in self.range_iterators]
starts, ends, values = zip(*[i.next() for i in iterators])
starts = list(starts)
ends = list(ends)
values = list(values)
while start < end:
min_end = min(ends)
yield start, min_end, values
start = min_end
for i, iterator in enumerate(iterators):
if ends[i] == min_end:
starts[i], ends[i], values[i] = iterator.next()
def __getitem__(self, index):
return [i[index] for i in self.range_iterators]
class ConstRunIterator(AbstractRunIterator):
'''Iterate over a constant value without creating a RunList.'''
def __init__(self, length, value):
self.length = length
self.value = value
def next(self):
yield 0, self.length, self.value
def ranges(self, start, end):
yield start, end, self.value
def __getitem__(self, index):
return self.value