from typing import Optional
import pyglet
from pyglet.text import Label
from arcade.gui.widgets.layout import UIAnchorLayout
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 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,
align="left",
dpi=None,
multiline: bool = False,
size_hint=None,
size_hint_min=None,
size_hint_max=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=width,
bold=bold,
italic=italic,
stretch=stretch,
align=align,
anchor_y="bottom", # position text bottom left, to fit into scissor box
dpi=dpi,
multiline=multiline,
**kwargs,
)
super().__init__(
x=x,
y=y,
width=width or self.layout.content_width,
height=height or self.layout.content_height,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
**kwargs,
)
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 + 1,
self.layout.content_height + base_height + 1,
)
@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, 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()
[docs]class UITextWidget(UIAnchorLayout):
"""
Adds the ability to add text to a widget.
The text can be placed within the widget using UIAnchorLayout parameters with `place_text()`.
"""
def __init__(self, text: str = "", **kwargs):
super().__init__(text=text, **kwargs)
self._label = UILabel(
text=text,
multiline=True,
width=1000
) # width 1000 try to prevent line wrap
self.add(self._label)
self.ui_label.fit_content()
[docs] def place_text(self,
anchor_x: Optional[str] = None,
align_x: float = 0,
anchor_y: Optional[str] = None,
align_y: float = 0,
**kwargs):
"""
This allows to place widgets text within the widget using UIAnchorLayout parameters.
"""
self.remove(self._label)
self.add(
child=self._label,
anchor_x=anchor_x,
align_x=align_x,
anchor_y=anchor_y,
align_y=align_y,
**kwargs
)
@property
def text(self):
return self._label.text
@text.setter
def text(self, value):
self.ui_label.text = value
self.ui_label.fit_content()
self.trigger_render()
@property
def ui_label(self) -> UILabel:
return self._label
@property
def label(self) -> Label:
return self._label.layout
[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
"""
# move layout one pixel into the scissor box, so the caret is also shown at position 0
LAYOUT_OFFSET = 1
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,
**kwargs,
):
super().__init__(
x=x,
y=y,
width=width,
height=height,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
**kwargs,
)
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 - self.LAYOUT_OFFSET, height, multiline=multiline
)
self.layout.x += self.LAYOUT_OFFSET
self.caret = Caret(self.layout, color=(0, 0, 0))
self.caret.visible = False
self._blink_state = self._get_caret_blink_state()
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_full_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 - self.LAYOUT_OFFSET, 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 - self.LAYOUT_OFFSET, 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 - self.LAYOUT_OFFSET, layout.height
if layout_size != self.content_size:
layout.begin_update()
layout.width = self.content_width - self.LAYOUT_OFFSET
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: Optional[float] = None,
size_hint=None,
size_hint_min=None,
size_hint_max=None,
**kwargs,
):
super().__init__(
x=x,
y=y,
width=width,
height=height,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
**kwargs,
)
# 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