# -*- 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