Ajout du GUI
This commit is contained in:
1016
kivy/core/text/__init__.py
Normal file
1016
kivy/core/text/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
kivy/core/text/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/markup.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/markup.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pango.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pango.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pil.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pil.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_pygame.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_pygame.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc
Normal file
BIN
kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
879
kivy/core/text/markup.py
Normal file
879
kivy/core/text/markup.py
Normal file
@@ -0,0 +1,879 @@
|
||||
'''
|
||||
Text Markup
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
|
||||
Added `font_context`, `font_features` and `text_language` (Pango only)
|
||||
|
||||
We provide a simple text-markup for inline text styling. The syntax look the
|
||||
same as the `BBCode <http://en.wikipedia.org/wiki/BBCode>`_.
|
||||
|
||||
A tag is defined as ``[tag]``, and should have a corresponding
|
||||
``[/tag]`` closing tag. For example::
|
||||
|
||||
[b]Hello [color=ff0000]world[/color][/b]
|
||||
|
||||
The following tags are available:
|
||||
|
||||
``[b][/b]``
|
||||
Activate bold text
|
||||
``[i][/i]``
|
||||
Activate italic text
|
||||
``[u][/u]``
|
||||
Underlined text
|
||||
``[s][/s]``
|
||||
Strikethrough text
|
||||
``[font=<str>][/font]``
|
||||
Change the font (note: this refers to a TTF file or registered alias)
|
||||
``[font_context=<str>][/font_context]``
|
||||
Change context for the font, use string value "none" for isolated context.
|
||||
``[font_family=<str>][/font_family]``
|
||||
Font family to request for drawing. This is only valid when using a
|
||||
font context, and takes precedence over `[font]`. See
|
||||
:class:`kivy.uix.label.Label` for details.
|
||||
``[font_features=<str>][/font_features]``
|
||||
OpenType font features, in CSS format, this is passed straight
|
||||
through to Pango. The effects of requesting a feature depends on loaded
|
||||
fonts, library versions, etc. Pango only, requires v1.38 or later.
|
||||
``[size=<size>][/size]``
|
||||
Change the font size. <size> should be an integer, optionally with a
|
||||
unit (i.e. ``16sp``)
|
||||
``[color=#<color>][/color]``
|
||||
Change the text color
|
||||
``[ref=<str>][/ref]``
|
||||
Add an interactive zone. The reference + all the word box inside the
|
||||
reference will be available in :attr:`MarkupLabel.refs`
|
||||
``[anchor=<str>]``
|
||||
Put an anchor in the text. You can get the position of your anchor within
|
||||
the text with :attr:`MarkupLabel.anchors`
|
||||
``[sub][/sub]``
|
||||
Display the text at a subscript position relative to the text before it.
|
||||
``[sup][/sup]``
|
||||
Display the text at a superscript position relative to the text before it.
|
||||
``[text_language=<str>][/text_language]``
|
||||
Language of the text, this is an RFC-3066 format language tag (as string),
|
||||
for example "en_US", "zh_CN", "fr" or "ja". This can impact font selection,
|
||||
metrics and rendering. For example, the same bytes of text can look
|
||||
different for `ur` and `ar` languages, though both use Arabic script.
|
||||
Use the string `'none'` to revert to locale detection. Pango only.
|
||||
|
||||
If you need to escape the markup from the current text, use
|
||||
:func:`kivy.utils.escape_markup`.
|
||||
'''
|
||||
|
||||
__all__ = ('MarkupLabel', )
|
||||
|
||||
import re
|
||||
from kivy.properties import dpi2px
|
||||
from kivy.parser import parse_color
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.text import Label, LabelBase
|
||||
from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine
|
||||
from copy import copy
|
||||
from functools import partial
|
||||
|
||||
# We need to do this trick when documentation is generated
|
||||
MarkupLabelBase = Label
|
||||
if Label is None:
|
||||
MarkupLabelBase = LabelBase
|
||||
|
||||
|
||||
class MarkupLabel(MarkupLabelBase):
|
||||
'''Markup text label.
|
||||
|
||||
See module documentation for more information.
|
||||
'''
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
self._style_stack = {}
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
super(MarkupLabel, self).__init__(*largs, **kwargs)
|
||||
self._internal_size = 0, 0
|
||||
self._cached_lines = []
|
||||
|
||||
@property
|
||||
def refs(self):
|
||||
'''Get the bounding box of all the ``[ref=...]``::
|
||||
|
||||
{ 'refA': ((x1, y1, x2, y2), (x1, y1, x2, y2)), ... }
|
||||
'''
|
||||
return self._refs
|
||||
|
||||
@property
|
||||
def anchors(self):
|
||||
'''Get the position of all the ``[anchor=...]``::
|
||||
|
||||
{ 'anchorA': (x, y), 'anchorB': (x, y), ... }
|
||||
'''
|
||||
return self._anchors
|
||||
|
||||
@property
|
||||
def markup(self):
|
||||
'''Return the text with all the markup split::
|
||||
|
||||
>>> MarkupLabel('[b]Hello world[/b]').markup
|
||||
>>> ('[b]', 'Hello world', '[/b]')
|
||||
|
||||
'''
|
||||
s = re.split(r'(\[.*?\])', self.label)
|
||||
s = [x for x in s if x != '']
|
||||
return s
|
||||
|
||||
def _push_style(self, k):
|
||||
if k not in self._style_stack:
|
||||
self._style_stack[k] = []
|
||||
self._style_stack[k].append(self.options[k])
|
||||
|
||||
def _pop_style(self, k):
|
||||
if k not in self._style_stack or len(self._style_stack[k]) == 0:
|
||||
Logger.warning('Label: pop style stack without push')
|
||||
return
|
||||
v = self._style_stack[k].pop()
|
||||
self.options[k] = v
|
||||
|
||||
def render(self, real=False):
|
||||
options = copy(self.options)
|
||||
if not real:
|
||||
ret = self._pre_render()
|
||||
else:
|
||||
ret = self._render_real()
|
||||
self.options = options
|
||||
return ret
|
||||
|
||||
def _pre_render(self):
|
||||
# split markup, words, and lines
|
||||
# result: list of word with position and width/height
|
||||
# during the first pass, we don't care about h/valign
|
||||
self._cached_lines = lines = []
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
clipped = False
|
||||
w = h = 0
|
||||
uw, uh = self.text_size
|
||||
spush = self._push_style
|
||||
spop = self._pop_style
|
||||
options = self.options
|
||||
options['_ref'] = None
|
||||
options['_anchor'] = None
|
||||
options['script'] = 'normal'
|
||||
shorten = options['shorten']
|
||||
# if shorten, then don't split lines to fit uw, because it will be
|
||||
# flattened later when shortening and broken up lines if broken
|
||||
# mid-word will have space mid-word when lines are joined
|
||||
uw_temp = None if shorten else uw
|
||||
xpad = options['padding_x']
|
||||
uhh = (None if uh is not None and options['valign'] != 'top' or
|
||||
options['shorten'] else uh)
|
||||
options['strip'] = options['strip'] or options['halign'] == 'justify'
|
||||
find_base_dir = Label.find_base_direction
|
||||
base_dir = options['base_direction']
|
||||
self._resolved_base_dir = None
|
||||
for item in self.markup:
|
||||
if item == '[b]':
|
||||
spush('bold')
|
||||
options['bold'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/b]':
|
||||
spop('bold')
|
||||
self.resolve_font_name()
|
||||
elif item == '[i]':
|
||||
spush('italic')
|
||||
options['italic'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/i]':
|
||||
spop('italic')
|
||||
self.resolve_font_name()
|
||||
elif item == '[u]':
|
||||
spush('underline')
|
||||
options['underline'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/u]':
|
||||
spop('underline')
|
||||
self.resolve_font_name()
|
||||
elif item == '[s]':
|
||||
spush('strikethrough')
|
||||
options['strikethrough'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/s]':
|
||||
spop('strikethrough')
|
||||
self.resolve_font_name()
|
||||
elif item[:6] == '[size=':
|
||||
item = item[6:-1]
|
||||
try:
|
||||
if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
|
||||
size = dpi2px(item[:-2], item[-2:])
|
||||
else:
|
||||
size = int(item)
|
||||
except ValueError:
|
||||
raise
|
||||
size = options['font_size']
|
||||
spush('font_size')
|
||||
options['font_size'] = size
|
||||
elif item == '[/size]':
|
||||
spop('font_size')
|
||||
elif item[:7] == '[color=':
|
||||
color = parse_color(item[7:-1])
|
||||
spush('color')
|
||||
options['color'] = color
|
||||
elif item == '[/color]':
|
||||
spop('color')
|
||||
elif item[:6] == '[font=':
|
||||
fontname = item[6:-1]
|
||||
spush('font_name')
|
||||
options['font_name'] = fontname
|
||||
self.resolve_font_name()
|
||||
elif item == '[/font]':
|
||||
spop('font_name')
|
||||
self.resolve_font_name()
|
||||
elif item[:13] == '[font_family=':
|
||||
spush('font_family')
|
||||
options['font_family'] = item[13:-1]
|
||||
elif item == '[/font_family]':
|
||||
spop('font_family')
|
||||
elif item[:14] == '[font_context=':
|
||||
fctx = item[14:-1]
|
||||
if not fctx or fctx.lower() == 'none':
|
||||
fctx = None
|
||||
spush('font_context')
|
||||
options['font_context'] = fctx
|
||||
elif item == '[/font_context]':
|
||||
spop('font_context')
|
||||
elif item[:15] == '[font_features=':
|
||||
spush('font_features')
|
||||
options['font_features'] = item[15:-1]
|
||||
elif item == '[/font_features]':
|
||||
spop('font_features')
|
||||
elif item[:15] == '[text_language=':
|
||||
lang = item[15:-1]
|
||||
if not lang or lang.lower() == 'none':
|
||||
lang = None
|
||||
spush('text_language')
|
||||
options['text_language'] = lang
|
||||
elif item == '[/text_language]':
|
||||
spop('text_language')
|
||||
elif item[:5] == '[sub]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'subscript'
|
||||
elif item == '[/sub]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[sup]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'superscript'
|
||||
elif item == '[/sup]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[ref=':
|
||||
ref = item[5:-1]
|
||||
spush('_ref')
|
||||
options['_ref'] = ref
|
||||
elif item == '[/ref]':
|
||||
spop('_ref')
|
||||
elif not clipped and item[:8] == '[anchor=':
|
||||
options['_anchor'] = item[8:-1]
|
||||
elif not clipped:
|
||||
item = item.replace('&bl;', '[').replace(
|
||||
'&br;', ']').replace('&', '&')
|
||||
if not base_dir:
|
||||
base_dir = self._resolved_base_dir = find_base_dir(item)
|
||||
opts = copy(options)
|
||||
extents = self.get_cached_extents()
|
||||
opts['space_width'] = extents(' ')[0]
|
||||
w, h, clipped = layout_text(
|
||||
item, lines, (w, h), (uw_temp, uhh),
|
||||
opts, extents,
|
||||
append_down=True,
|
||||
complete=False
|
||||
)
|
||||
|
||||
if len(lines): # remove any trailing spaces from the last line
|
||||
old_opts = self.options
|
||||
self.options = copy(opts)
|
||||
w, h, clipped = layout_text(
|
||||
'', lines, (w, h), (uw_temp, uhh),
|
||||
self.options, self.get_cached_extents(),
|
||||
append_down=True,
|
||||
complete=True
|
||||
)
|
||||
self.options = old_opts
|
||||
|
||||
self.is_shortened = False
|
||||
if shorten:
|
||||
options['_ref'] = None # no refs for you!
|
||||
options['_anchor'] = None
|
||||
w, h, lines = self.shorten_post(lines, w, h)
|
||||
self._cached_lines = lines
|
||||
# when valign is not top, for markup we layout everything (text_size[1]
|
||||
# is temporarily set to None) and after layout cut to size if too tall
|
||||
elif uh != uhh and h > uh and len(lines) > 1:
|
||||
if options['valign'] == 'bottom':
|
||||
i = 0
|
||||
while i < len(lines) - 1 and h > uh:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
else: # middle
|
||||
i = 0
|
||||
top = int(h / 2. + uh / 2.) # remove extra top portion
|
||||
while i < len(lines) - 1 and h > top:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
i = len(lines) - 1 # remove remaining bottom portion
|
||||
while i and h > uh:
|
||||
h -= lines[i].h
|
||||
i -= 1
|
||||
del lines[i + 1:]
|
||||
|
||||
# now justify the text
|
||||
if options['halign'] == 'justify' and uw is not None:
|
||||
# XXX: update refs to justified pos
|
||||
# when justify, each line should've been stripped already
|
||||
split = partial(re.split, re.compile('( +)'))
|
||||
uww = uw - 2 * xpad
|
||||
chr = type(self.text)
|
||||
space = chr(' ')
|
||||
empty = chr('')
|
||||
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
words = line.words
|
||||
# if there's nothing to justify, we're done
|
||||
if (not line.w or int(uww - line.w) <= 0 or not len(words) or
|
||||
line.is_last_line):
|
||||
continue
|
||||
|
||||
done = False
|
||||
parts = [None, ] * len(words) # contains words split by space
|
||||
idxs = [None, ] * len(words) # indices of the space in parts
|
||||
# break each word into spaces and add spaces until it's full
|
||||
# do first round of split in case we don't need to split all
|
||||
for w in range(len(words)):
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w] = split(word.text)
|
||||
idxs[w] = [v for v in range(len(p)) if
|
||||
p[v].startswith(' ')]
|
||||
# now we have the indices of the spaces in split list
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# there's not a single space in the line?
|
||||
if not any(idxs):
|
||||
continue
|
||||
|
||||
# now keep adding spaces to already split words until done
|
||||
while not done:
|
||||
for w in range(len(words)):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w]
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# if not completely full, push last words to right edge
|
||||
diff = int(uww - line.w)
|
||||
if diff > 0:
|
||||
# find the last word that had a space
|
||||
for w in range(len(words) - 1, -1, -1):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
break
|
||||
old_opts = self.options
|
||||
self.options = word.options
|
||||
word = words[w]
|
||||
# split that word into left/right and push right till uww
|
||||
l_text = empty.join(parts[w][:idxs[w][-1]])
|
||||
r_text = empty.join(parts[w][idxs[w][-1]:])
|
||||
left = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(l_text)[0],
|
||||
word.lh,
|
||||
l_text
|
||||
)
|
||||
right = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(r_text)[0],
|
||||
word.lh,
|
||||
r_text
|
||||
)
|
||||
left.lw = max(left.lw, word.lw + diff - right.lw)
|
||||
self.options = old_opts
|
||||
|
||||
# now put words back together with right/left inserted
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
words[w] = right
|
||||
words.insert(w, left)
|
||||
else:
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
line.w = uww
|
||||
w = max(w, uww)
|
||||
|
||||
self._internal_size = w, h
|
||||
if uw:
|
||||
w = uw
|
||||
if uh:
|
||||
h = uh
|
||||
if h > 1 and w < 2:
|
||||
w = 2
|
||||
if w < 1:
|
||||
w = 1
|
||||
if h < 1:
|
||||
h = 1
|
||||
return int(w), int(h)
|
||||
|
||||
def render_lines(self, lines, options, render_text, y, size):
|
||||
xpad = options['padding_x']
|
||||
w = size[0]
|
||||
halign = options['halign']
|
||||
refs = self._refs
|
||||
anchors = self._anchors
|
||||
base_dir = options['base_direction'] or self._resolved_base_dir
|
||||
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
||||
|
||||
for layout_line in lines: # for plain label each line has only one str
|
||||
lw, lh = layout_line.w, layout_line.h
|
||||
x = xpad
|
||||
if halign == 'center':
|
||||
x = int((w - lw) / 2.)
|
||||
elif halign == 'right' or auto_halign_r:
|
||||
x = max(0, int(w - lw - xpad))
|
||||
layout_line.x = x
|
||||
layout_line.y = y
|
||||
psp = pph = 0
|
||||
for word in layout_line.words:
|
||||
options = self.options = word.options
|
||||
# the word height is not scaled by line_height, only lh was
|
||||
wh = options['line_height'] * word.lh
|
||||
# calculate sub/super script pos
|
||||
if options['script'] == 'superscript':
|
||||
script_pos = max(0, psp if psp else self.get_descent())
|
||||
psp = script_pos
|
||||
pph = wh
|
||||
elif options['script'] == 'subscript':
|
||||
script_pos = min(lh - wh, ((psp + pph) - wh)
|
||||
if pph else (lh - wh))
|
||||
pph = wh
|
||||
psp = script_pos
|
||||
else:
|
||||
script_pos = (lh - wh) / 1.25
|
||||
psp = pph = 0
|
||||
if len(word.text):
|
||||
render_text(word.text, x, y + script_pos)
|
||||
|
||||
# should we record refs ?
|
||||
ref = options['_ref']
|
||||
if ref is not None:
|
||||
if ref not in refs:
|
||||
refs[ref] = []
|
||||
refs[ref].append((x, y, x + word.lw, y + wh))
|
||||
|
||||
# Should we record anchors?
|
||||
anchor = options['_anchor']
|
||||
if anchor is not None:
|
||||
if anchor not in anchors:
|
||||
anchors[anchor] = (x, y)
|
||||
x += word.lw
|
||||
y += lh
|
||||
return y
|
||||
|
||||
def shorten_post(self, lines, w, h, margin=2):
|
||||
''' Shortens the text to a single line according to the label options.
|
||||
|
||||
This function operates on a text that has already been laid out because
|
||||
for markup, parts of text can have different size and options.
|
||||
|
||||
If :attr:`text_size` [0] is None, the lines are returned unchanged.
|
||||
Otherwise, the lines are converted to a single line fitting within the
|
||||
constrained width, :attr:`text_size` [0].
|
||||
|
||||
:params:
|
||||
|
||||
`lines`: list of `LayoutLine` instances describing the text.
|
||||
`w`: int, the width of the text in lines, including padding.
|
||||
`h`: int, the height of the text in lines, including padding.
|
||||
`margin` int, the additional space left on the sides. This is in
|
||||
addition to :attr:`padding_x`.
|
||||
|
||||
:returns:
|
||||
3-tuple of (xw, h, lines), where w, and h is similar to the input
|
||||
and contains the resulting width / height of the text, including
|
||||
padding. lines, is a list containing a single `LayoutLine`, which
|
||||
contains the words for the line.
|
||||
'''
|
||||
|
||||
def n(line, c):
|
||||
''' A function similar to text.find, except it's an iterator that
|
||||
returns successive occurrences of string c in list line. line is
|
||||
not a string, but a list of LayoutWord instances that we walk
|
||||
from left to right returning the indices of c in the words as we
|
||||
encounter them. Note that the options can be different among the
|
||||
words.
|
||||
|
||||
:returns:
|
||||
3-tuple: the index of the word in line, the index of the
|
||||
occurrence in word, and the extents (width) of the combined
|
||||
words until this occurrence, not including the occurrence char.
|
||||
If no more are found it returns (-1, -1, total_w) where total_w
|
||||
is the full width of all the words.
|
||||
'''
|
||||
total_w = 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.find, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield w, i, total_w + self.get_extents(word.text[:i])[0]
|
||||
i = f(i + 1)
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def p(line, c):
|
||||
''' Similar to the `n` function, except it returns occurrences of c
|
||||
from right to left in the list, line, similar to rfind.
|
||||
'''
|
||||
total_w = 0
|
||||
offset = 0 if len(c) else 1
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.rfind, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield (w, i, total_w +
|
||||
self.get_extents(word.text[i + 1:])[0])
|
||||
if i:
|
||||
i = f(0, i - offset)
|
||||
else:
|
||||
if not c:
|
||||
self.options = word.options
|
||||
yield (w, -1, total_w +
|
||||
self.get_extents(word.text)[0])
|
||||
break
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def n_restricted(line, uw, c):
|
||||
''' Similar to the function `n`, except it only returns the first
|
||||
occurrence and it's not an iterator. Furthermore, if the first
|
||||
occurrence doesn't fit within width uw, it returns the index of
|
||||
whatever amount of text will still fit in uw.
|
||||
|
||||
:returns:
|
||||
similar to the function `n`, except it's a 4-tuple, with the
|
||||
last element a boolean, indicating if we had to clip the text
|
||||
to fit in uw (True) or if the whole text until the first
|
||||
occurrence fitted in uw (False).
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
f = partial(word.text.find, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[:i])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
i = len(word.text)
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
e = 0
|
||||
while e != i and total_w + extents(word.text[:e])[0] <= uw:
|
||||
e += 1
|
||||
e = max(0, e - 1)
|
||||
return w, e, total_w + extents(word.text[:e])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
def p_restricted(line, uw, c):
|
||||
''' Similar to `n_restricted`, except it returns the first
|
||||
occurrence starting from the right, like `p`.
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
f = partial(word.text.rfind, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[i + 1:])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
s = len(word.text) - 1
|
||||
while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
|
||||
s -= 1
|
||||
return w, s, total_w + extents(word.text[s + 1:])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
textwidth = self.get_cached_extents()
|
||||
uw = self.text_size[0]
|
||||
if uw is None:
|
||||
return w, h, lines
|
||||
old_opts = copy(self.options)
|
||||
uw = max(0, int(uw - old_opts['padding_x'] * 2 - margin))
|
||||
chr = type(self.text)
|
||||
ssize = textwidth(' ')
|
||||
c = old_opts['split_str']
|
||||
line_height = old_opts['line_height']
|
||||
xpad, ypad = old_opts['padding_x'], old_opts['padding_y']
|
||||
dir = old_opts['shorten_from'][0]
|
||||
|
||||
# flatten lines into single line
|
||||
line = []
|
||||
last_w = 0
|
||||
for l in range(len(lines)):
|
||||
# concatenate (non-empty) inside lines with a space
|
||||
this_line = lines[l]
|
||||
if last_w and this_line.w and not this_line.line_wrap:
|
||||
line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' ')))
|
||||
last_w = this_line.w or last_w
|
||||
for word in this_line.words:
|
||||
if word.lw:
|
||||
line.append(word)
|
||||
|
||||
# if that fits, just return the flattened line
|
||||
lw = sum([word.lw for word in line])
|
||||
if lw <= uw:
|
||||
lh = max([word.lh for word in line] + [0]) * line_height
|
||||
self.is_shortened = False
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line)]
|
||||
)
|
||||
|
||||
elps_opts = copy(old_opts)
|
||||
if 'ellipsis_options' in old_opts:
|
||||
elps_opts.update(old_opts['ellipsis_options'])
|
||||
|
||||
# Set new opts for ellipsis
|
||||
self.options = elps_opts
|
||||
# find the size of ellipsis that'll fit
|
||||
elps_s = textwidth('...')
|
||||
if elps_s[0] > uw: # even ellipsis didn't fit...
|
||||
self.is_shortened = True
|
||||
s = textwidth('..')
|
||||
if s[0] <= uw:
|
||||
return (
|
||||
s[0] + 2 * xpad,
|
||||
s[1] * line_height + 2 * ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '..')])]
|
||||
)
|
||||
|
||||
else:
|
||||
s = textwidth('.')
|
||||
return (
|
||||
s[0] + 2 * xpad,
|
||||
s[1] * line_height + 2 * ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '.')])]
|
||||
)
|
||||
|
||||
elps = LayoutWord(elps_opts, elps_s[0], elps_s[1], '...')
|
||||
uw -= elps_s[0]
|
||||
# Restore old opts
|
||||
self.options = old_opts
|
||||
|
||||
# now find the first left and right words that fit
|
||||
w1, e1, l1, clipped1 = n_restricted(line, uw, c)
|
||||
w2, s2, l2, clipped2 = p_restricted(line, uw, c)
|
||||
|
||||
if dir != 'l': # center or right
|
||||
line1 = None
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take first
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1],
|
||||
last_text))
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if line1:
|
||||
line1.append(elps)
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
if dir == 'r':
|
||||
f = n(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1) # first word should match
|
||||
ww1, ee1, l1 = next(f)
|
||||
while l2 + l1 <= uw:
|
||||
w1, e1 = ww1, ee1
|
||||
ww1, ee1, l1 = next(f)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # center
|
||||
f = n(line, c) # iterator
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1)
|
||||
assert next(f_inv)[:-1] == (w2, s2)
|
||||
while True:
|
||||
if l1 <= l2:
|
||||
ww1, ee1, l1 = next(f) # hypothesize that next fit
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w1, e1 = ww1, ee1
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else:
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w2, s2 = ww2, ss2
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # left
|
||||
line1 = [elps]
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take last
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if len(line1) != 1:
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f_inv)[:-1] == (w2, s2) # last word should match
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
while l2 + l1 <= uw:
|
||||
w2, s2 = ww2, ss2
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
|
||||
# now add back the left half
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
if len(last_text):
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
|
||||
line1.append(elps)
|
||||
|
||||
# now add back the right half
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
if len(first_text):
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
if uw < lw:
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + 2 * xpad,
|
||||
lh + 2 * ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
BIN
kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so
Executable file
BIN
kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so
Executable file
Binary file not shown.
13
kivy/core/text/text_layout.pxd
Normal file
13
kivy/core/text/text_layout.pxd
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
cdef class LayoutWord:
|
||||
cdef public object text
|
||||
cdef public int lw, lh
|
||||
cdef public dict options
|
||||
|
||||
|
||||
cdef class LayoutLine:
|
||||
cdef public int x, y, w, h
|
||||
cdef public int line_wrap # whether this line wraps from last line
|
||||
cdef public int is_last_line # in a paragraph
|
||||
cdef public list words
|
||||
145
kivy/core/text/text_pango.py
Normal file
145
kivy/core/text/text_pango.py
Normal file
@@ -0,0 +1,145 @@
|
||||
'''
|
||||
Pango text provider
|
||||
===================
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
|
||||
.. warning::
|
||||
The low-level Pango API is experimental, and subject to change without
|
||||
notice for as long as this warning is present.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
1. Install pangoft2 (`apt install libfreetype6-dev libpango1.0-dev
|
||||
libpangoft2-1.0-0`) or ensure it is available in pkg-config
|
||||
2. Recompile kivy. Check that pangoft2 is found `use_pangoft2 = 1`
|
||||
3. Test it! Enforce the text core renderer to pango using environment variable:
|
||||
`export KIVY_TEXT=pango`
|
||||
|
||||
This has been tested on OSX and Linux, Python 3.6.
|
||||
|
||||
|
||||
Font context types for FontConfig+FreeType2 backend
|
||||
---------------------------------------------------
|
||||
|
||||
* `system://` - `FcInitLoadConfigAndFonts()`
|
||||
* `systemconfig://` - `FcInitLoadConfig()`
|
||||
* `directory://<PATH>` - `FcInitLoadConfig()` + `FcAppFontAddDir()`
|
||||
* `fontconfig://<PATH>` - `FcConfigCreate()` + `FcConfigParseAndLoad()`
|
||||
* Any other context name - `FcConfigCreate()`
|
||||
|
||||
|
||||
Low-level Pango access
|
||||
----------------------
|
||||
|
||||
Since Kivy currently does its own text layout, the Label and TextInput widgets
|
||||
do not take full advantage of Pango. For example, line breaks do not take
|
||||
language/script into account, and switching alignment per paragraph (for bi-
|
||||
directional text) is not supported. For advanced i18n requirements, we provide
|
||||
a simple wrapper around PangoLayout that you can use to render text.
|
||||
|
||||
* https://developer.gnome.org/pango/1.40/pango-Layout-Objects.html
|
||||
* https://developer.gnome.org/pango/1.40/PangoMarkupFormat.html
|
||||
* See the `kivy/core/text/_text_pango.pyx` file @ `cdef class KivyPangoLayout`
|
||||
for more information. Not all features of PangoLayout are implemented.
|
||||
|
||||
.. python::
|
||||
from kivy.core.window import Window # OpenGL must be initialized
|
||||
from kivy.core.text._text_pango import KivyPangoLayout
|
||||
layout = KivyPangoLayout('system://')
|
||||
layout.set_markup('<span font="20">Hello <b>World!</b></span>')
|
||||
tex = layout.render_as_Texture()
|
||||
|
||||
|
||||
Known limitations
|
||||
-----------------
|
||||
|
||||
* Pango versions older than v1.38 has not been tested. It may work on
|
||||
some systems with older pango and newer FontConfig/FreeType2 versions.
|
||||
* Kivy's text layout is used, not Pango. This means we do not use Pango's
|
||||
line-breaking feature (which is superior to Kivy's), and we can't use
|
||||
Pango's bidirectional cursor helpers in TextInput.
|
||||
* Font family collissions can happen. For example, if you use a `system://`
|
||||
context and add a custom `Arial.ttf`, using `arial` as the `font_family`
|
||||
may or may not draw with your custom font (depending on whether or not
|
||||
there is already a system-wide "arial" font installed)
|
||||
* Rendering is inefficient; the normal way to integrate Pango would be
|
||||
using a dedicated PangoLayout per widget. This is not currently practical
|
||||
due to missing abstractions in Kivy core (in the current implementation,
|
||||
we have a dedicated PangoLayout *per font context,* which is rendered
|
||||
once for each LayoutWord)
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPango', )
|
||||
|
||||
from types import MethodType
|
||||
from os.path import isfile
|
||||
from kivy.resources import resource_find
|
||||
from kivy.core.text import LabelBase, FontContextManagerBase
|
||||
from kivy.core.text._text_pango import (
|
||||
KivyPangoRenderer,
|
||||
kpango_get_extents,
|
||||
kpango_get_ascent,
|
||||
kpango_get_descent,
|
||||
kpango_find_base_dir,
|
||||
kpango_font_context_exists,
|
||||
kpango_font_context_create,
|
||||
kpango_font_context_destroy,
|
||||
kpango_font_context_add_font,
|
||||
kpango_font_context_list,
|
||||
kpango_font_context_list_custom,
|
||||
kpango_font_context_list_families)
|
||||
|
||||
|
||||
class LabelPango(LabelBase):
|
||||
|
||||
_font_family_support = True
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
self.get_extents = MethodType(kpango_get_extents, self)
|
||||
self.get_ascent = MethodType(kpango_get_ascent, self)
|
||||
self.get_descent = MethodType(kpango_get_descent, self)
|
||||
super(LabelPango, self).__init__(*largs, **kwargs)
|
||||
|
||||
find_base_direction = staticmethod(kpango_find_base_dir)
|
||||
|
||||
def _render_begin(self):
|
||||
self._rdr = KivyPangoRenderer(*self._size)
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
self._rdr.render(self, text, x, y)
|
||||
|
||||
def _render_end(self):
|
||||
imgdata = self._rdr.get_ImageData()
|
||||
del self._rdr
|
||||
return imgdata
|
||||
|
||||
|
||||
class PangoFontContextManager(FontContextManagerBase):
|
||||
create = staticmethod(kpango_font_context_create)
|
||||
exists = staticmethod(kpango_font_context_exists)
|
||||
destroy = staticmethod(kpango_font_context_destroy)
|
||||
list = staticmethod(kpango_font_context_list)
|
||||
list_families = staticmethod(kpango_font_context_list_families)
|
||||
list_custom = staticmethod(kpango_font_context_list_custom)
|
||||
|
||||
@staticmethod
|
||||
def add_font(font_context, filename, autocreate=True, family=None):
|
||||
if not autocreate and not PangoFontContextManager.exists(font_context):
|
||||
raise Exception("FontContextManager: Attempt to add font file "
|
||||
"'{}' to non-existing context '{}' without "
|
||||
"autocreate.".format(filename, font_context))
|
||||
if not filename:
|
||||
raise Exception("FontContextManager: Cannot add empty font file")
|
||||
if not isfile(filename):
|
||||
filename = resource_find(filename)
|
||||
if not isfile(filename):
|
||||
if not filename.endswith('.ttf'):
|
||||
filename = resource_find('{}.ttf'.format(filename))
|
||||
if filename and isfile(filename):
|
||||
return kpango_font_context_add_font(font_context, filename)
|
||||
raise Exception("FontContextManager: Attempt to add non-existent "
|
||||
"font file: '{}' to context '{}'"
|
||||
.format(filename, font_context))
|
||||
60
kivy/core/text/text_pil.py
Normal file
60
kivy/core/text/text_pil.py
Normal file
@@ -0,0 +1,60 @@
|
||||
'''
|
||||
Text PIL: Draw text with PIL
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPIL', )
|
||||
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
||||
|
||||
from kivy.compat import text_type
|
||||
from kivy.core.text import LabelBase
|
||||
from kivy.core.image import ImageData
|
||||
|
||||
# used for fetching extends before creature image surface
|
||||
default_font = ImageFont.load_default()
|
||||
|
||||
|
||||
class LabelPIL(LabelBase):
|
||||
_cache = {}
|
||||
|
||||
def _select_font(self):
|
||||
fontsize = int(self.options['font_size'])
|
||||
fontname = self.options['font_name_r']
|
||||
try:
|
||||
id = '%s.%s' % (text_type(fontname), text_type(fontsize))
|
||||
except UnicodeDecodeError:
|
||||
id = '%s.%s' % (fontname, fontsize)
|
||||
|
||||
if id not in self._cache:
|
||||
font = ImageFont.truetype(fontname, fontsize)
|
||||
self._cache[id] = font
|
||||
|
||||
return self._cache[id]
|
||||
|
||||
def get_extents(self, text):
|
||||
font = self._select_font()
|
||||
w, h = font.getsize(text)
|
||||
return w, h
|
||||
|
||||
def get_cached_extents(self):
|
||||
return self._select_font().getsize
|
||||
|
||||
def _render_begin(self):
|
||||
# create a surface, context, font...
|
||||
self._pil_im = Image.new('RGBA', self._size, color=(255, 255, 255, 0))
|
||||
self._pil_draw = ImageDraw.Draw(self._pil_im)
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
color = tuple([int(c * 255) for c in self.options['color']])
|
||||
self._pil_draw.text((int(x), int(y)),
|
||||
text, font=self._select_font(), fill=color)
|
||||
|
||||
def _render_end(self):
|
||||
data = ImageData(self._size[0], self._size[1],
|
||||
self._pil_im.mode.lower(), self._pil_im.tobytes())
|
||||
|
||||
del self._pil_im
|
||||
del self._pil_draw
|
||||
|
||||
return data
|
||||
117
kivy/core/text/text_pygame.py
Normal file
117
kivy/core/text/text_pygame.py
Normal file
@@ -0,0 +1,117 @@
|
||||
'''
|
||||
Text Pygame: Draw text with pygame
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPygame', )
|
||||
|
||||
from kivy.compat import PY2
|
||||
from kivy.core.text import LabelBase
|
||||
from kivy.core.image import ImageData
|
||||
from kivy.utils import deprecated
|
||||
|
||||
try:
|
||||
import pygame
|
||||
except:
|
||||
raise
|
||||
|
||||
pygame_cache = {}
|
||||
pygame_font_handles = {}
|
||||
pygame_cache_order = []
|
||||
|
||||
# init pygame font
|
||||
try:
|
||||
pygame.ftfont.init()
|
||||
except:
|
||||
pygame.font.init()
|
||||
|
||||
|
||||
class LabelPygame(LabelBase):
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, *largs, **kwargs):
|
||||
super(LabelPygame, self).__init__(*largs, **kwargs)
|
||||
|
||||
def _get_font_id(self):
|
||||
return '|'.join([str(self.options[x]) for x in
|
||||
('font_size', 'font_name_r', 'bold', 'italic')])
|
||||
|
||||
def _get_font(self):
|
||||
fontid = self._get_font_id()
|
||||
if fontid not in pygame_cache:
|
||||
# try first the file if it's a filename
|
||||
font_handle = fontobject = None
|
||||
fontname = self.options['font_name_r']
|
||||
ext = fontname.rsplit('.', 1)
|
||||
if len(ext) == 2:
|
||||
# try to open the font if it has an extension
|
||||
font_handle = open(fontname, 'rb')
|
||||
fontobject = pygame.font.Font(font_handle,
|
||||
int(self.options['font_size']))
|
||||
|
||||
# fallback to search a system font
|
||||
if fontobject is None:
|
||||
# try to search the font
|
||||
font = pygame.font.match_font(
|
||||
self.options['font_name_r'].replace(' ', ''),
|
||||
bold=self.options['bold'],
|
||||
italic=self.options['italic'])
|
||||
|
||||
# fontobject
|
||||
fontobject = pygame.font.Font(font,
|
||||
int(self.options['font_size']))
|
||||
pygame_cache[fontid] = fontobject
|
||||
pygame_font_handles[fontid] = font_handle
|
||||
pygame_cache_order.append(fontid)
|
||||
|
||||
# to prevent too much file open, limit the number of opened fonts to 64
|
||||
while len(pygame_cache_order) > 64:
|
||||
popid = pygame_cache_order.pop(0)
|
||||
del pygame_cache[popid]
|
||||
font_handle = pygame_font_handles.pop(popid)
|
||||
if font_handle is not None:
|
||||
font_handle.close()
|
||||
|
||||
return pygame_cache[fontid]
|
||||
|
||||
def get_ascent(self):
|
||||
return self._get_font().get_ascent()
|
||||
|
||||
def get_descent(self):
|
||||
return self._get_font().get_descent()
|
||||
|
||||
def get_extents(self, text):
|
||||
return self._get_font().size(text)
|
||||
|
||||
def get_cached_extents(self):
|
||||
return self._get_font().size
|
||||
|
||||
def _render_begin(self):
|
||||
self._pygame_surface = pygame.Surface(self._size, pygame.SRCALPHA, 32)
|
||||
self._pygame_surface.fill((0, 0, 0, 0))
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
font = self._get_font()
|
||||
color = [c * 255 for c in self.options['color']]
|
||||
color[0], color[2] = color[2], color[0]
|
||||
try:
|
||||
text = font.render(text, True, color)
|
||||
text.set_colorkey(color)
|
||||
self._pygame_surface.blit(text, (x, y), None,
|
||||
pygame.BLEND_RGBA_ADD)
|
||||
except pygame.error:
|
||||
pass
|
||||
|
||||
def _render_end(self):
|
||||
w, h = self._size
|
||||
data = ImageData(w, h,
|
||||
'rgba', self._pygame_surface.get_buffer().raw)
|
||||
|
||||
del self._pygame_surface
|
||||
|
||||
return data
|
||||
50
kivy/core/text/text_sdl2.py
Normal file
50
kivy/core/text/text_sdl2.py
Normal file
@@ -0,0 +1,50 @@
|
||||
'''
|
||||
SDL2 text provider
|
||||
==================
|
||||
|
||||
Based on SDL2 + SDL2_ttf
|
||||
'''
|
||||
|
||||
__all__ = ('LabelSDL2', )
|
||||
|
||||
from kivy.compat import PY2
|
||||
from kivy.core.text import LabelBase
|
||||
try:
|
||||
from kivy.core.text._text_sdl2 import (_SurfaceContainer, _get_extents,
|
||||
_get_fontdescent, _get_fontascent)
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'text', 'sdl2', 'kivy.core.text._text_sdl2')
|
||||
raise
|
||||
|
||||
|
||||
class LabelSDL2(LabelBase):
|
||||
|
||||
def _get_font_id(self):
|
||||
return '|'.join([str(self.options[x]) for x
|
||||
in ('font_size', 'font_name_r', 'bold',
|
||||
'italic', 'underline', 'strikethrough')])
|
||||
|
||||
def get_extents(self, text):
|
||||
try:
|
||||
if PY2:
|
||||
text = text.encode('UTF-8')
|
||||
except:
|
||||
pass
|
||||
return _get_extents(self, text)
|
||||
|
||||
def get_descent(self):
|
||||
return _get_fontdescent(self)
|
||||
|
||||
def get_ascent(self):
|
||||
return _get_fontascent(self)
|
||||
|
||||
def _render_begin(self):
|
||||
self._surface = _SurfaceContainer(self._size[0], self._size[1])
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
self._surface.render(self, text, x, y)
|
||||
|
||||
def _render_end(self):
|
||||
return self._surface.get_data()
|
||||
Reference in New Issue
Block a user