Shofel2_T124_python/venv/lib/python3.10/site-packages/asciimatics/renderers/base.py

272 lines
8.6 KiB
Python
Raw Normal View History

2024-05-25 16:45:07 +00:00
# -*- coding: utf-8 -*-
"""
This module provides common code for all Renderers.
"""
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from builtins import object
from abc import ABCMeta, abstractproperty, abstractmethod
from future.utils import with_metaclass
import re
from wcwidth.wcwidth import wcswidth
from asciimatics.screen import Screen, TemporaryCanvas
from asciimatics.constants import COLOUR_REGEX
#: Attribute conversion table for the ${c,a} form of attributes for
#: :py:obj:`~.Screen.paint`.
ATTRIBUTES = {
"1": Screen.A_BOLD,
"2": Screen.A_NORMAL,
"3": Screen.A_REVERSE,
"4": Screen.A_UNDERLINE,
}
class Renderer(with_metaclass(ABCMeta, object)):
"""
A Renderer is simply a class that will return one or more text renderings
for display by an Effect.
In the simple case, this can be a single string that contains some
unchanging content - e.g. a simple text message.
It can also represent a sequence of strings that can be played one after
the other to make a simple animation sequence - e.g. a rotating globe.
"""
@abstractproperty
def max_width(self):
"""
:return: The max width of the rendered text (across all images if an animated renderer).
"""
@abstractproperty
def rendered_text(self):
"""
:return: The next image and colour map in the sequence as a tuple.
"""
@abstractproperty
def images(self):
"""
:return: An iterator of all the images in the Renderer.
"""
@abstractproperty
def max_height(self):
"""
:return: The max height of the rendered text (across all images if an animated renderer).
"""
def __repr__(self):
"""
:returns: a plain string representation of the next rendered image.
"""
return "\n".join(self.rendered_text[0])
class StaticRenderer(Renderer):
"""
A StaticRenderer is a Renderer that can create all possible images in
advance. After construction the images will not change, but can by cycled
for animation purposes.
This class will also convert text like ${c,a,b} into colour c, attribute a
and background b for any subsequent text in the line, thus allowing
multi-coloured text. The attribute and background are optional.
"""
# Regular expression for use to find colour sequences in multi-colour text.
# It should match ${n}, ${m,n} or ${m,n,o}
_colour_sequence = re.compile(COLOUR_REGEX)
def __init__(self, images=None, animation=None):
"""
:param images: An optional set of ascii images to be rendered.
:param animation: A function to pick the image (from images) to be
rendered for any given frame.
"""
super(StaticRenderer, self).__init__()
self._images = images if images is not None else []
self._index = 0
self._max_width = 0
self._max_height = 0
self._animation = animation
self._colour_map = None
self._plain_images = []
def _convert_images(self):
"""
Convert any images into a more Screen-friendly format.
"""
self._plain_images = []
self._colour_map = []
for image in self._images:
colour_map = []
new_image = []
for line in image.split("\n"):
new_line = ""
attributes = (None, None, None)
colours = []
while len(line) > 0:
match = self._colour_sequence.match(line)
if match is None:
new_line += line[0]
colours.append(attributes)
line = line[1:]
else:
# The regexp either matches:
# - 2,3,4 for ${c,a,b}
# - 5,6 for ${c,a}
# - 7 for ${c}.
if match.group(2) is not None:
attributes = (int(match.group(2)),
ATTRIBUTES[match.group(3)],
int(match.group(4)))
elif match.group(5) is not None:
attributes = (int(match.group(5)),
ATTRIBUTES[match.group(6)],
None)
else:
attributes = (int(match.group(7)), 0, None)
line = match.group(8)
new_image.append(new_line)
colour_map.append(colours)
self._plain_images.append(new_image)
self._colour_map.append(colour_map)
@property
def images(self):
"""
:return: An iterator of all the images in the Renderer.
"""
if len(self._plain_images) <= 0:
self._convert_images()
return iter(self._plain_images)
@property
def rendered_text(self):
"""
:return: The next image and colour map in the sequence as a tuple.
"""
if len(self._plain_images) <= 0:
self._convert_images()
if self._animation is None:
index = self._index
self._index += 1
if self._index >= len(self._plain_images):
self._index = 0
else:
index = self._animation()
return (self._plain_images[index],
self._colour_map[index])
@property
def max_height(self):
"""
:return: The max height of the rendered text (across all images if an animated renderer).
"""
if len(self._plain_images) <= 0:
self._convert_images()
if self._max_height == 0:
for image in self._plain_images:
self._max_height = max(len(image), self._max_height)
return self._max_height
@property
def max_width(self):
"""
:return: The max width of the rendered text (across all images if an animated renderer).
"""
if len(self._plain_images) <= 0:
self._convert_images()
if self._max_width == 0:
for image in self._plain_images:
new_max = max([wcswidth(x) for x in image])
self._max_width = max(new_max, self._max_width)
return self._max_width
class DynamicRenderer(with_metaclass(ABCMeta, Renderer)):
"""
A DynamicRenderer is a Renderer that creates each image as requested. It
has a defined maximum size on construction.
"""
def __init__(self, height, width, clear=True):
"""
:param height: The max height of the rendered image.
:param width: The max width of the rendered image.
"""
super(DynamicRenderer, self).__init__()
self._must_clear = clear
self._canvas = TemporaryCanvas(height, width)
def _clear(self):
"""
Clear the current image.
"""
# self._canvas.clear_buffer(Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK)
self._canvas.clear_buffer(None, 0, 0)
def _write(self, text, x, y, colour=Screen.COLOUR_WHITE,
attr=Screen.A_NORMAL, bg=Screen.COLOUR_BLACK):
"""
Write some text to the specified location in the current image.
:param text: The text to be added.
:param x: The X coordinate in the image.
:param y: The Y coordinate in the image.
:param colour: The colour of the text to add.
:param attr: The attribute of the image.
:param bg: The background colour of the text to add.
This is only kept for back compatibility. Direct access to the canvas methods is
preferred.
"""
self._canvas.print_at(text, x, y, colour, attr, bg)
@property
def _plain_image(self):
return self._canvas.plain_image
@property
def _colour_map(self):
return self._canvas.colour_map
@abstractmethod
def _render_now(self):
"""
Common method to render the latest image.
:returns: A tuple of the plain image and the colour map as per
:py:meth:`.rendered_text`.
"""
@property
def images(self):
# We can't return all, so just return the latest rendered image.
return [self.rendered_text[0]]
@property
def rendered_text(self):
if self._must_clear:
self._clear()
return self._render_now()
@property
def max_height(self):
return self._canvas.height
@property
def max_width(self):
return self._canvas.width