Source code for pygame_menu.widgets.widget.label

"""
pygame-menu
https://github.com/ppizarror/pygame-menu

LABEL
Label class, adds a simple text to the Menu.
"""

__all__ = [
    'Label',
    'LabelManager'
]

import pygame
import pygame_menu
import textwrap
import time

from abc import ABC
from pygame_menu.utils import assert_color, warn, uuid4, make_surface
from pygame_menu.widgets.core.widget import Widget, AbstractWidgetManager

from pygame_menu._types import Any, CallbackType, List, Union, Tuple, Optional, \
    ColorType, ColorInputType, EventVectorType, Callable

LabelTitleGeneratorType = Optional[Callable[[], str]]


# noinspection PyMissingOrEmptyDocstring
[docs]class Label(Widget): """ Label widget. .. note:: Label accepts all transformations. :param title: Label title/text :param label_id: Label ID :param onselect: Function when selecting the label widget :param wordwrap: Wraps label if newline is found on widget :param leading: Font leading for ``wordwrap``. If ``None`` retrieves from widget font :param max_nlines: Number of maximum lines for ``wordwrap``. If ``None`` the number is dynamically computed. If exceded, ``label.get_overflow_lines()`` will return the lines not displayed """ _last_underline: List[Union[str, Optional[Tuple[ColorType, int, int]]]] _leading: Optional[int] _lines: List[str] _max_nlines: Optional[int] _overflow_lines: List[str] # Store how many lines are overflowed starting end _title_generator: LabelTitleGeneratorType _wordwrap: bool def __init__( self, title: Any, label_id: str = '', onselect: CallbackType = None, wordwrap: bool = False, leading: Optional[int] = None, max_nlines: Optional[int] = None ) -> None: assert isinstance(leading, (type(None), int)) assert isinstance(max_nlines, (type(None), int)) super(Label, self).__init__( title=title, onselect=onselect, widget_id=label_id ) self._last_underline = ['', None] # deco id, (color, offset, width) self._leading = leading self._lines = [] # Lines of text displayed self._max_nlines = max_nlines self._overflow_lines = [] self._title_generator = None self._wordwrap = wordwrap
[docs] def add_underline( self, color: ColorInputType, offset: int, width: int, force_render: bool = False ) -> 'Label': """ Adds an underline to text. This is added if widget is rendered. Underline is only enabled for non wordwrap label. :param color: Underline color :param offset: Underline offset :param width: Underline width :param force_render: If ``True`` force widget render after addition :return: Self reference """ assert not self._wordwrap, 'underline is not enabled for wordwrap is active' color = assert_color(color) assert isinstance(offset, int) assert isinstance(width, int) and width > 0 self._last_underline[1] = (color, offset, width) if force_render: self._force_render() return self
[docs] def remove_underline(self) -> 'Label': """ Remove underline of the label. :return: Self reference """ assert not self._wordwrap, 'underline is not enabled for wordwrap is active' if self._last_underline[0] != '': self._decorator.remove(self._last_underline[0]) self._last_underline[0] = '' return self
def _apply_font(self) -> None: pass def _draw(self, surface: 'pygame.Surface') -> None: # The minimal width of any surface is 1px, so the background will be a line if self._title == '': return surface.blit(self._surface, self._rect.topleft)
[docs] def set_title_generator(self, generator: LabelTitleGeneratorType) -> 'Label': """ Set a title generator. This function is executed each time the label updates, returning a new title (string) which replaces the current label title. The generator does not take any input as argument. :param generator: Function which generates a new text status :return: Self reference """ if generator is not None: assert callable(generator) self._title_generator = generator # Update update widgets menu_update_widgets = self._get_menu_update_widgets() if generator is None and self in menu_update_widgets: menu_update_widgets.remove(self) if generator is not None and self not in menu_update_widgets: menu_update_widgets.append(self) return self
[docs] def set_title(self, title: str) -> 'Label': super(Label, self).set_title(title) if self._title_generator is not None: if self._verbose: warn( f'{self.get_class_id()} title generator is not None, thus, the new' f' title "{title}" will be overridden after next update' ) return self
def _get_leading(self) -> int: """ Computes the font leading. :return: Leading """ assert self._font return ( self._font.get_linesize() if self._leading is None else self._leading )
[docs] def get_lines(self) -> List[str]: """ Return the lines of text displayed. Each new line belongs to an item on list. :return: List of displayed lines """ return self._lines
@staticmethod def _wordwrap_line( line: str, font: pygame.font.Font, max_width: int, tab_size: int, ) -> List[str]: """ Wordwraps line. :param line: Line :param font: Font :param max_width: Max width :param tab_size: Tab size :return: List of strings """ final_lines = [] words = line.split(' ') i, current_line = 0, '' while True: split_line = False for i, _ in enumerate(words): current_line = ' '.join(words[:i + 1]) current_line = current_line.replace('\t', ' ' * tab_size) current_line_size = font.size(current_line) if current_line_size[0] > max_width: split_line = True break if split_line: i = i if i > 0 else 1 final_lines.append(' '.join(words[:i])) words = words[i:] else: final_lines.append(current_line) break return final_lines def _get_max_container_width(self) -> int: """ Return the maximum label container width. It can be the column width, menu width or frame width if horizontal. :return: Container width """ menu = self._menu if menu is None: return 0 try: # noinspection PyProtectedMember max_width = menu._column_widths[self.get_col_row_index()[0]] except IndexError: max_width = menu.get_width(inner=True) return max_width - self._padding[1] - self._padding[3] - self._selection_effect.get_width()
[docs] def get_overflow_lines(self) -> List[str]: """ Return the overflow lines ir ``wordwrap`` is active and ``max_nlines`` is set. :return: Lines not displayed """ assert self._wordwrap, 'wordwrap must be enabled' assert isinstance(self._max_nlines, int), 'max_nlines must be defined' return self._overflow_lines
def _render(self) -> Optional[bool]: if not self._render_hash_changed(self._title, self._font_color, self._visible, self._menu, self._font, self._last_underline[1], self._padding, self._selection_effect.get_width()): return True self._lines = [] # Generate surface if not self._wordwrap: self._surface = self._render_string(self._title, self._font_color) self._lines.append(self._title) else: self._overflow_lines = [] if self._font is None or self._menu is None: self._surface = make_surface(0, 0, alpha=True) else: lines = self._title.split('\n') lines = sum( ( self._wordwrap_line( line=line, font=self._font, max_width=self._get_max_container_width(), tab_size=self._tab_size ) for line in lines ), [] ) num_lines = len(lines) if isinstance(self._max_nlines, int): if num_lines > self._max_nlines: for j in range(num_lines - self._max_nlines): self._overflow_lines.append(lines[num_lines - j - 1]) num_lines = min(num_lines, self._max_nlines) self._surface = make_surface( max(self._font.size(line)[0] for line in lines), num_lines * self._get_leading(), alpha=True ) for n_line, line in enumerate(lines): line_surface = self._render_string(line, self._font_color) self._surface.blit( line_surface, pygame.Rect( 0, n_line * self._get_leading(), self._rect.width, self._rect.height ) ) self._lines.append(line) if n_line + 1 == num_lines: break # Update rect object self._apply_transforms() self._rect.width, self._rect.height = self._surface.get_size() # Apply underline if not self._wordwrap: self.remove_underline() if self._last_underline[1] is not None: w = self._surface.get_width() h = self._surface.get_height() color, offset, width = self._last_underline[1] if w > 0 and h > 0: self._last_underline[0] = self._decorator.add_line( pos1=(-w / 2, h / 2 + offset), pos2=(w / 2, h / 2 + offset), color=color, width=width ) self.force_menu_surface_update() def update(self, events: EventVectorType) -> bool: # If generator is not None if self._title_generator is not None: gen_title = self._title_generator() assert isinstance(gen_title, str), \ f'object generated by the title generator ({gen_title}) is not string-type' self._title = gen_title self._render() self.apply_update_callbacks(events) for event in events: if self._check_mouseover(event): break return False
class LabelManager(AbstractWidgetManager, ABC): """ Label manager. """ # noinspection PyProtectedMember def label( self, title: Any, label_id: str = '', max_char: int = 0, onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None, selectable: bool = False, wordwrap: bool = False, **kwargs ) -> Union['pygame_menu.widgets.Label', List['pygame_menu.widgets.Label']]: """ Add a simple text to the Menu. If ``onselect`` is defined, the callback is executed as follows, where ``selected`` is a boolean representing the selected status: .. code-block:: python onselect(selected, widget, menu) kwargs (Optional) - ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_ - ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color - ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px - ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color - ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px - ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals` - ``border_width`` (int) – Border width in px. If ``0`` disables the border - ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over - ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows - ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating - ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color - ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color - ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path - ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color - ``font_shadow_offset`` (int) – Font shadow offset in px - ``font_shadow_position`` (str) – Font shadow position, see locals for position - ``font_shadow`` (bool) – Font shadow is enabled or disabled - ``font_size`` (int) – Font size of the widget - ``leading`` (int) - Font leading for ``wordwrap``. If ``None`` retrieves from widget font - ``margin`` (tuple, list) – Widget (left, bottom) margin in px - ``max_nlines`` (int) - Number of maximum lines for ``wordwrap``. If ``None`` the number is dynamically computed. If exceded, ``label.get_overflow_lines()`` will return the lines not displayed - ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left) - ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color - ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect. Applied only if ``selectable`` is ``True`` - ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow - ``shadow_radius`` (int) - Border radius of the shadow - ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'`` - ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled - ``tab_size`` (int) – Width of a tab character - ``underline_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Color of the underline. If ``None`` use the same color of the text - ``underline_offset`` (int) – Vertical offset in px. ``2`` by default - ``underline_width`` (int) – Underline width in px. ``2`` by default - ``underline`` (bool) – Enables text underline, using a properly placed decoration. ``False`` by default .. note:: All theme-related optional kwargs use the default Menu theme if not defined. .. note:: This is applied only to the base Menu (not the currently displayed, stored in ``_current`` pointer); for such behaviour apply to :py:meth:`pygame_menu.menu.Menu.get_current` object. :param title: Text to be displayed :param label_id: ID of the label :param max_char: Split the title in several labels if the string length exceeds ``max_char``; ``0``: don't split, ``-1``: split to Menu width :param onselect: Callback executed when selecting the widget; only executed if ``selectable`` is ``True`` :param selectable: Label accepts user selection; useful to move along the Menu using label selection :param wordwrap: Wraps label if newline is found on widget. If ``False`` the manager splits the string and creates a list of widgets, else, the widget itself splits and updates the height :param kwargs: Optional keyword arguments :return: Widget object, or List of widgets if the text overflows :rtype: :py:class:`pygame_menu.widgets.Label`, :py:class:`typing.List` [:py:class:`pygame_menu.widgets.Label`] """ assert isinstance(label_id, str) assert isinstance(max_char, int) assert isinstance(selectable, bool) assert max_char >= -1 title = str(title) if len(label_id) == 0: label_id = uuid4() # If newline detected, split in two new lines if '\n' in title and not wordwrap: title = title.split('\n') widgets = [] for t in title: wig = self.label( title=t, label_id=label_id + '+' + str(len(widgets) + 1), max_char=max_char, onselect=onselect, selectable=selectable, **kwargs ) if isinstance(wig, list): for w in wig: widgets.append(w) else: widgets.append(wig) return widgets # Wrap text to Menu width (imply additional calls to render functions) if max_char < 0: if len(title) > 0: dummy_attrs = self._filter_widget_attributes(kwargs.copy()) dummy = pygame_menu.widgets.Label(title=title) self._configure_widget(dummy, **dummy_attrs) max_char = int(1.0 * self._menu.get_width(inner=True) * len(title) / dummy.get_width()) else: max_char = 0 leading = kwargs.pop('leading', None) max_nlines = kwargs.pop('max_nlines', None) # If no overflow if len(title) <= max_char or max_char == 0 or wordwrap: attributes = self._filter_widget_attributes(kwargs) # Filter additional parameters underline = kwargs.pop('underline', False) underline_color = kwargs.pop('underline_color', attributes['font_color']) underline_offset = kwargs.pop('underline_offset', 1) underline_width = kwargs.pop('underline_width', 1) widget = Label( label_id=label_id, onselect=onselect, title=title, wordwrap=wordwrap and not underline, leading=leading, max_nlines=max_nlines ) widget.is_selectable = selectable self._check_kwargs(kwargs) self._configure_widget(widget=widget, **attributes) if underline: widget.add_underline(underline_color, underline_offset, underline_width) self._append_widget(widget) else: self._menu._check_id_duplicated(label_id) # Before adding + LEN widget = [] for line in textwrap.wrap(title, max_char): widget.append( self.label( title=line, label_id=label_id + '+' + str(len(widget) + 1), max_char=max_char, onselect=onselect, selectable=selectable, wordwrap=wordwrap, leading=leading, max_nlines=max_nlines, **kwargs ) ) return widget def clock( self, clock_format: str = '%Y/%m/%d %H:%M:%S', clock_id: str = '', onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None, selectable: bool = False, title_format: str = '{0}', wordwrap: bool = False, **kwargs ) -> 'pygame_menu.widgets.Label': """ Add a clock label to the Menu. This creates a Label with a text generator that request a string from ``time.strftime`` module using ``clock_format``. Commonly used format codes: - **%Y** – Year with century as a decimal number - **%m** – Month as a decimal number [01, 12] - **%d** – Day of the month as a decimal number [01, 31] - **%H** – Hour (24-hour clock) as a decimal number [00, 23] - **%M** – Minute as a decimal number [00, 59] - **%S** – Second as a decimal number [00, 61] - **%z** – Time zone offset from UTC - **%a** – Locale's abbreviated weekday name - **%A** – Locale's full weekday name - **%b** – Locale's abbreviated month name - **%B** – Locale's full month name - **%c** – Locale's appropriate date and time representation - **%I** – Hour (12-hour clock) as a decimal number [01, 12] - **%p** – Locale's equivalent of either AM or PM If ``onselect`` is defined, the callback is executed as follows, where ``selected`` is a boolean representing the selected status: .. code-block:: python onselect(selected, widget, menu) kwargs (Optional) - ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_ - ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color - ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px - ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color - ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px - ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals` - ``border_width`` (int) – Border width in px. If ``0`` disables the border - ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over - ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows - ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating - ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color - ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color - ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path - ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color - ``font_shadow_offset`` (int) – Font shadow offset in px - ``font_shadow_position`` (str) – Font shadow position, see locals for position - ``font_shadow`` (bool) – Font shadow is enabled or disabled - ``font_size`` (int) – Font size of the widget - ``leading`` (int) - Font leading for ``wordwrap``. If ``None`` retrieves from widget font - ``margin`` (tuple, list) – Widget (left, bottom) margin in px - ``max_nlines`` (int) - Number of maximum lines for ``wordwrap``. If ``None`` the number is dynamically computed. If exceded, ``label.get_overflow_lines()`` will return the lines not displayed - ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left) - ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color - ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect. Applied only if ``selectable`` is ``True`` - ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow - ``shadow_radius`` (int) - Border radius of the shadow - ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'`` - ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled - ``tab_size`` (int) – Width of a tab character - ``underline_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Color of the underline. If ``None`` use the same color of the text - ``underline_offset`` (int) – Vertical offset in px. ``2`` by default - ``underline_width`` (int) – Underline width in px. ``2`` by default - ``underline`` (bool) – Enables text underline, using a properly placed decoration. ``False`` by default .. note:: All theme-related optional kwargs use the default Menu theme if not defined. .. note:: This is applied only to the base Menu (not the currently displayed, stored in ``_current`` pointer); for such behaviour apply to :py:meth:`pygame_menu.menu.Menu.get_current` object. :param clock_format: Format of clock used by ``time.strftime`` :param clock_id: ID of the clock :param onselect: Callback executed when selecting the widget; only executed if ``selectable`` is ``True`` :param selectable: Label accepts user selection; useful to move along the Menu using label selection :param title_format: Title format which accepts ``{0}`` as the string from ``time.strftime``, for example, ``'My Clock {0}'`` can be a title format :param wordwrap: Wraps label if newline is found on widget. If ``False`` the manager splits the string and creates a list of widgets, else, the widget itself splits and updates the height :param kwargs: Optional keyword arguments :return: Widget object :rtype: :py:class:`pygame_menu.widgets.Label` """ label = self.label( title='', label_id=clock_id, onselect=onselect, selectable=selectable, wordwrap=wordwrap, **kwargs ) assert isinstance(title_format, str) and '{0}' in title_format assert not isinstance(label, list) label.set_title_generator(lambda: title_format.format(time.strftime(clock_format))) label.update([]) return label