Source code for arcade.gui.widgets.text

from typing import Optional

import pyglet
from pyglet.event import EVENT_UNHANDLED, EVENT_HANDLED
from pyglet.text.caret import Caret
from pyglet.text.document import AbstractDocument

import arcade
from arcade.gui.events import (
    UIEvent,
    UIMousePressEvent,
    UITextEvent,
    UITextMotionEvent,
    UITextMotionSelectEvent,
    UIMouseEvent,
    UIMouseDragEvent,
    UIMouseScrollEvent,
)
from arcade.gui.property import bind
from arcade.gui.widgets import UIWidget, Surface, Rect


[docs]class UILabel(UIWidget): """A simple text label. Also supports multiline text. In case you want to scroll text use a :class:`UITextArea` By default a :class:`UILabel` will fit its initial content, if the text changed use :meth:`UILabel.fit_content` to adjust the size. :param float x: x coordinate of bottom left :param float y: y coordinate of bottom left :param float width: width of widget. Defaults to text width if not specified. :param float height: height of widget. Defaults to text height if not specified. :param str text: text of the label. :param font_name: a list of fonts to use. Program will start at the beginning of the list and keep trying to load fonts until success. :param float font_size: size of font. :param arcade.Color text_color: Color of font. :param bool bold: Bold font style. :param bool italic: Italic font style. :param bool stretch: Stretch font style. :param str anchor_x: Anchor point of the X coordinate: one of ``"left"``, ``"center"`` or ``"right"``. :param str anchor_y: Anchor point of the Y coordinate: one of ``"bottom"``, ``"baseline"``, ``"center"`` or ``"top"``. :param str align: Horizontal alignment of text on a line, only applies if a width is supplied. One of ``"left"``, ``"center"`` or ``"right"``. :param float dpi: Resolution of the fonts in this layout. Defaults to 96. :param bool multiline: if multiline is true, a \\n will start a new line. A UITextWidget with multiline of true is the same thing as UITextArea. :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested :param size_hint_min: min width and height in pixel :param size_hint_max: max width and height in pixel :param style: Not used. """ def __init__( self, x: float = 0, y: float = 0, width: Optional[float] = None, height: Optional[float] = None, text: str = "", font_name=("Arial",), font_size: float = 12, text_color: arcade.Color = (255, 255, 255, 255), bold=False, italic=False, stretch=False, anchor_x="left", anchor_y="bottom", align="left", dpi=None, multiline: bool = False, size_hint=None, size_hint_min=None, size_hint_max=None, style=None, **kwargs, ): # Use Pyglet's Label for text rendering self.layout = pyglet.text.Label( text=text, font_name=font_name, font_size=font_size, color=arcade.get_four_byte_color(text_color), width=None, height=None, bold=bold, italic=italic, stretch=stretch, anchor_x=anchor_x, anchor_y=anchor_y, align=align, dpi=dpi, multiline=multiline, ) super().__init__( x, y, width or self.layout.content_width, height or self.layout.content_height, size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, ) self.layout.width = width self.layout.height = height bind(self, "rect", self._update_layout)
[docs] def fit_content(self): """ Sets the width and height of this UIWidget to contain the whole text. """ base_width = self.padding_left + self.padding_right + 2 * self.border_width base_height = self.padding_top + self.padding_bottom + 2 * self.border_width self.rect = self.rect.resize( self.layout.content_width + base_width, self.layout.content_height + base_height, )
@property def text(self): return self.layout.text @text.setter def text(self, value): self.layout.text = value self._update_layout() self.trigger_full_render() def _update_layout(self): # Update Pyglet layout size layout = self.layout layout_size = layout.width, layout.height if layout_size != self.content_size: layout.begin_update() layout.position = 0, 0 # layout always drawn in scissor box layout.width = self.content_width layout.height = self.content_height layout.end_update() def do_render(self, surface: Surface): self.prepare_render(surface) with surface.ctx.pyglet_rendering(): self.layout.draw()
# class _Arcade_Caret(Caret): # def _update(self, line=None, update_ideal_x=True): # if line is None: # line = self._layout.get_line_from_position(self._position) # self._ideal_line = None # else: # self._ideal_line = line # x, y = self._layout.get_point_from_position(self._position, line) # if update_ideal_x: # self._ideal_x = x # # # x -= self._layout.view_x # # y -= self._layout.view_y # # add 1px offset to make caret visible on line start # x += self._layout.x + 1 # # y += self._layout.y + self._layout.height # # font = self._layout.document.get_font(max(0, self._position - 1)) # self._list.position[:] = [x, y + font.descent, x, y + font.ascent] # # if self._mark is not None: # self._layout.set_selection( # min(self._position, self._mark), max(self._position, self._mark) # ) # # self._layout.ensure_line_visible(line) # self._layout.ensure_x_visible(x)
[docs]class UIInputText(UIWidget): """ An input field the user can type text into. :param float x: x coordinate of bottom left :param float y: y coordinate of bottom left :param width: width of widget :param height: height of widget :param text: Text to show :param font_name: string or tuple of font names, to load :param font_size: size of the text :param text_color: color of the text :param multiline: support for multiline :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested :param size_hint_min: min width and height in pixel :param size_hint_max: max width and height in pixel :param style: not used """ def __init__( self, x: float = 0, y: float = 0, width: float = 100, height: float = 50, text: str = "", font_name=("Arial",), font_size: float = 12, text_color: arcade.Color = (0, 0, 0, 255), multiline=False, size_hint=None, size_hint_min=None, size_hint_max=None, style=None, **kwargs, ): super().__init__( x, y, width, height, size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, ) # fixme workaround for https://github.com/pyglet/pyglet/issues/529 init_text = False if text == "": init_text = True text = " " self._active = False self._text_color = text_color if len(text_color) == 4 else (*text_color, 255) self.doc: AbstractDocument = pyglet.text.decode_text(text) self.doc.set_style( 0, len(text), dict(font_name=font_name, font_size=font_size, color=self._text_color), ) self.layout = pyglet.text.layout.IncrementalTextLayout( self.doc, width, height, multiline=multiline ) self.caret = Caret(self.layout, color=(0, 0, 0)) self._blink_state = self._get_caret_blink_state() if init_text: self.text = "" def _get_caret_blink_state(self): return self.caret._visible and self._active and self.caret._blink_visible def on_update(self, dt): # Only trigger render if blinking state changed current_state = self._get_caret_blink_state() if self._blink_state != current_state: self._blink_state = current_state self.trigger_render() def on_event(self, event: UIEvent) -> Optional[bool]: # if not active, check to activate, return if not self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): self._active = True self.trigger_full_render() self.caret.on_activate() self.caret.position = len(self.doc.text) return EVENT_UNHANDLED # if active check to deactivate if self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): x, y = event.x - self.x, event.y - self.y self.caret.on_mouse_press(x, y, event.button, event.modifiers) else: self._active = False self.trigger_full_render() self.caret.on_deactivate() return EVENT_UNHANDLED # if active pass all non press events to caret if self._active: # Act on events if active if isinstance(event, UITextEvent): self.caret.on_text(event.text) self.trigger_full_render() elif isinstance(event, UITextMotionEvent): self.caret.on_text_motion(event.motion) self.trigger_full_render() elif isinstance(event, UITextMotionSelectEvent): self.caret.on_text_motion_select(event.selection) self.trigger_full_render() if isinstance(event, UIMouseEvent) and self.rect.collide_with_point( event.x, event.y ): x, y = event.x - self.x, event.y - self.y if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( x, y, event.dx, event.dy, event.buttons, event.modifiers ) self.trigger_full_render() elif isinstance(event, UIMouseScrollEvent): self.caret.on_mouse_scroll(x, y, event.scroll_x, event.scroll_y) self.trigger_full_render() if super().on_event(event): return EVENT_HANDLED return EVENT_UNHANDLED def _update_layout(self): # Update Pyglet layout size layout = self.layout layout_size = layout.width, layout.height if layout_size != self.content_size: layout.begin_update() layout.width = self.content_width layout.height = self.content_height layout.end_update() @property def text(self): return self.doc.text @text.setter def text(self, value): self.doc.text = value def do_render(self, surface: Surface): self._update_layout() self.prepare_render(surface) with surface.ctx.pyglet_rendering(): self.layout.draw()
[docs]class UITextArea(UIWidget): """ A text area for scrollable text. :param float x: x coordinate of bottom left :param float y: y coordinate of bottom left :param width: width of widget :param height: height of widget :param text: Text to show :param font_name: string or tuple of font names, to load :param font_size: size of the text :param text_color: color of the text :param multiline: support for multiline :param scroll_speed: speed of scrolling :param size_hint: Tuple of floats (0.0-1.0), how much space of the parent should be requested :param size_hint_min: min width and height in pixel :param size_hint_max: max width and height in pixel :param style: not used """ def __init__( self, x: float = 0, y: float = 0, width: float = 400, height: float = 40, text: str = "", font_name=("Arial",), font_size: float = 12, text_color: arcade.Color = (255, 255, 255, 255), multiline: bool = True, scroll_speed: float = None, size_hint=None, size_hint_min=None, size_hint_max=None, style=None, **kwargs, ): super().__init__( x, y, width, height, size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, ) # Set how fast the mouse scroll wheel will scroll text in the pane. # Measured in pixels per 'click' self.scroll_speed = scroll_speed if scroll_speed is not None else font_size self.doc: AbstractDocument = pyglet.text.decode_text(text) self.doc.set_style( 0, 12, dict( font_name=font_name, font_size=font_size, color=arcade.get_four_byte_color(text_color), ), ) self.layout = pyglet.text.layout.ScrollableTextLayout( self.doc, width=self.content_width, height=self.content_height, multiline=multiline, ) # bind(self, "rect", self._update_layout)
[docs] def fit_content(self): """ Sets the width and height of this UIWidget to contain the whole text. """ self.rect = Rect( self.x, self.y, self.layout.content_width, self.layout.content_height, )
@property def text(self): return self.doc.text @text.setter def text(self, value): self.doc.text = value self.trigger_render() def _update_layout(self): # Update Pyglet layout size layout = self.layout layout_size = layout.width, layout.height if layout_size != self.content_size: layout.begin_update() layout.width = self.content_width layout.height = self.content_height layout.end_update() def do_render(self, surface: Surface): self._update_layout() self.prepare_render(surface) with surface.ctx.pyglet_rendering(): self.layout.draw() def on_event(self, event: UIEvent) -> Optional[bool]: if isinstance(event, UIMouseScrollEvent): if self.rect.collide_with_point(event.x, event.y): self.layout.view_y += event.scroll_y * self.scroll_speed self.trigger_full_render() if super().on_event(event): return EVENT_HANDLED return EVENT_UNHANDLED