Source code for cairotft.widgets.marquee

# Copyright (c) 2015, Thomas Chiroux - Link Care Services
# 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 cairotft 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 HOLDER 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.
"""Text Marquee widget."""
import time

import cairocffi as cairo

from cairotft import linuxfb
from cairotft import transitions


[docs]class Marquee(): """A text marquee widget.""" def __init__(self, display_object, text, font_face, font_size, text_color, pos_x, pos_y, width, height, background_color=(1, 1, 1, 1), step=1, interval_time=0.05, transition=transitions.LinearTransition.ease_in, smooth=False): """Initialisation of the Marquee. :param display_object: the Display class instanciation. :type display_object: :class:`cairotft.tft.TftDisplay` :param str text: the text to display. :param font_face: the font face used for the text :type font_face: :class:`cairocffi.ToyFontFace` :param tuple text_color: a tuple of 4 float representing the rgba value of the text color. :param int font_size: the size of the font. :param int pos_x: x coordinates of the text box (top left corner) :param int pos_y: y coordinates of the text box (top left corner) :param int width: the width of the text box :param int height: the height of the text box :param tuple background_color: a tuple of 4 float representing the rgba value of the background color to repaint the icon. :param int step: number of unit we skip at each frame. (X chars if smooth is False, X pixels if smooth is True) :param float interval_time: the time in s between two frames TODO: instead of repainting with a color, save the background or paint in order: from bottom to top. """ self._stop = False self._showing = False self._pos = 0 self.display_object = display_object self.text = text self.full_text = self.text + " " + self.text self._shrinked_text = text self.font_face = font_face self.font_size = font_size self.text_color = text_color self.background_color = background_color self._old_text_color = text_color self._old_background_color = background_color self.pos_x = pos_x self.pos_y = pos_y self.width = width self.height = height self.step = step self.smooth = smooth self.max_offset = len(self.text) + 3 self.interval_time = interval_time self.transition = transition self._transition_time = None self._first_time = None self._should_scroll = False # if False, text does not scroll
[docs] def _smooth_init_buffer(self, ctx): """Initialise the buffer used in smooth text.""" # in smooth case, we need to instanciate specific buffers # at first, determine the width and height we need: ctx.set_font_size(self.font_size) ctx.set_font_face(self.font_face) (_, _, small_text_width, _, x_advance_text, _) = ctx.text_extents(self.text + " ") self.smooth_text_width = int(x_advance_text) self.max_offset = self.smooth_text_width (_, y_bearing, width, height, x_advance, _) = ctx.text_extents(self.full_text) self.smooth_full_height = int(height) self.smooth_full_width = int(max(x_advance, width)) self.smooth_pos_y = int(-y_bearing) stride = cairo.ImageSurface.format_stride_for_width( self.display_object.cairo_format, int(x_advance)) textbuffer = linuxfb.memory_buffer( int(stride * self.smooth_full_height)) self.smooth_textsurf = cairo.ImageSurface( format=self.display_object.cairo_format, width=self.smooth_full_width, height=self.smooth_full_height, data=textbuffer, stride=stride) # if the text does not fit inside the target box, we will scrool it if small_text_width > self.width: self._should_scroll = True self._transition_time = (self.interval_time * self.smooth_text_width / self.step) # creates the context for this memory surface and draw the text self.smooth_text_ctx = cairo.Context(self.smooth_textsurf) self._smooth_draw_text()
[docs] def _smooth_draw_text(self): """draw the text in the smooth text buffer. This is not display, only drawed in the smooth buffer. """ # background self.smooth_text_ctx.set_source_rgba(*self.background_color) self.smooth_text_ctx.rectangle(0, 0, self.smooth_full_width, self.smooth_full_height) self.smooth_text_ctx.fill() # text self.smooth_text_ctx.set_font_size(self.font_size) self.smooth_text_ctx.set_font_face(self.font_face) self.smooth_text_ctx.set_source_rgba(*self.text_color) self.smooth_text_ctx.move_to(0, self.smooth_pos_y) if self._should_scroll: self.smooth_text_ctx.show_text(self.full_text) else: self.smooth_text_ctx.show_text(self.text)
[docs] def _shrink_text(self, ctx): """based on the font size, calculate the width of the text. And eventually shrink the name if too long. """ # same font face here than the one displayed. if self._pos > 0: new_text = (self.full_text)[self._pos:] else: new_text = self.text[self._pos:] while True: (_, _, _, _, x_advance, _) = ctx.text_extents(new_text) if x_advance > self.width: new_text = new_text[:-1] # + '…' else: break self._shrinked_text = new_text if self._shrinked_text != self.text: self._should_scroll = True self._transition_time = (self.interval_time * len(self.text + " ") / self.step)
[docs] def change_color(self, color): """Change the text color. :param tuple color: a tuple of 4 float representing the rgba value of the text color. """ self._old_text_color = self.text_color self.text_color = color if self.smooth and self._showing: self._smooth_draw_text()
[docs] def change_background(self, background_color): """Change the background color. :param tuple background_color: a tuple of 4 float representing the rgba value of the background color to repaint the icon. """ self._old_background_color = self.background_color self.background_color = background_color if self.smooth and self._showing: self._smooth_draw_text()
[docs] def color_changed(self): """Return True is either text_color or background_color has changed. (since last paint) """ if (self._old_text_color != self.text_color or self._old_background_color != self.background_color): return True
[docs] def show(self, ctx, no_loop=False): """Show the text.""" if not self._stop and self._showing: # here at each frame, move the text and display it. if (self.color_changed or self._should_scroll) and not self.smooth: # erase the text box ctx.set_source_rgba(*self.background_color) ctx.rectangle(self.pos_x, self.pos_y, self.width, self.height) ctx.fill() # display text ctx.set_source_rgba(*self.text_color) ctx.set_font_size(self.font_size) ctx.set_font_face(self.font_face) self._shrink_text(ctx) ctx.move_to( self.pos_x, (self.pos_y + (self.height - self.font_size) / 2 + self.font_size) - 2) ctx.show_text(self._shrinked_text) self.display_object.blit() elif (self.color_changed or self._should_scroll) and self.smooth: # erase the text box ctx.set_source_rgba(*self.background_color) ctx.rectangle(self.pos_x, self.pos_y, self.width, self.height) ctx.fill() ctx.set_source_surface( self.smooth_textsurf.create_for_rectangle( self._pos, 0, self.width, self.smooth_full_height), self.pos_x, (self.pos_y + (self.height - self.smooth_full_height) / 2) + 1) ctx.paint() self.display_object.blit() if self._should_scroll: # only cycle when text is too long. now = time.time() if self._first_time is not None: transition_offset = self.transition( (now - self._first_time) / self._transition_time) else: transition_offset = self.transition(0) self._first_time = now self._pos = int(self.max_offset * transition_offset) if (now - self._first_time) > self._transition_time: self._first_time = now # recycle self._pos = int(self.max_offset * self.transition(0)) self._old_text_color = self.text_color self._old_background_color = self.background_color if not no_loop: self.display_object.io_loop.call_later( self.interval_time, self.show, ctx)
[docs] def start(self, ctx): """Start showing the marquee.""" if not self._showing: self._showing = True self._stop = False self._first_time = None if self.smooth: self._smooth_init_buffer(ctx) else: self._shrink_text(ctx) self.display_object.io_loop.call_soon( self.show, ctx)
[docs] def stop(self): """stop showing the marquee.""" self._stop = True self._showing = False self._first_time = None