Spaces:
Runtime error
Runtime error
"""Display different types of interactive widgets. | |
""" | |
import pyglet | |
from pyglet.event import EventDispatcher | |
from pyglet.graphics import Group | |
from pyglet.text.caret import Caret | |
from pyglet.text.layout import IncrementalTextLayout | |
class WidgetBase(EventDispatcher): | |
def __init__(self, x, y, width, height): | |
self._x = x | |
self._y = y | |
self._width = width | |
self._height = height | |
self._bg_group = None | |
self._fg_group = None | |
self.enabled = True | |
def update_groups(self, order): | |
pass | |
def x(self): | |
"""X coordinate of the widget. | |
:type: int | |
""" | |
return self._x | |
def x(self, value): | |
self._x = value | |
self._update_position() | |
def y(self): | |
"""Y coordinate of the widget. | |
:type: int | |
""" | |
return self._y | |
def y(self, value): | |
self._y = value | |
self._update_position() | |
def position(self): | |
"""The x, y coordinate of the widget as a tuple. | |
:type: tuple(int, int) | |
""" | |
return self._x, self._y | |
def position(self, values): | |
self._x, self._y = values | |
self._update_position() | |
def width(self): | |
"""Width of the widget. | |
:type: int | |
""" | |
return self._width | |
def height(self): | |
"""Height of the widget. | |
:type: int | |
""" | |
return self._height | |
def aabb(self): | |
"""Bounding box of the widget. | |
Expressed as (x, y, x + width, y + height) | |
:type: (int, int, int, int) | |
""" | |
return self._x, self._y, self._x + self._width, self._y + self._height | |
def value(self): | |
"""Query or set the Widget's value. | |
This property allows you to set the value of a Widget directly, without any | |
user input. This could be used, for example, to restore Widgets to a | |
previous state, or if some event in your program is meant to naturally | |
change the same value that the Widget controls. Note that events are not | |
dispatched when changing this property. | |
""" | |
raise NotImplementedError("Value depends on control type!") | |
def value(self, value): | |
raise NotImplementedError("Value depends on control type!") | |
def _check_hit(self, x, y): | |
return self._x < x < self._x + self._width and self._y < y < self._y + self._height | |
def _update_position(self): | |
raise NotImplementedError("Unable to reposition this Widget") | |
def on_mouse_press(self, x, y, buttons, modifiers): | |
pass | |
def on_mouse_release(self, x, y, buttons, modifiers): | |
pass | |
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): | |
pass | |
def on_mouse_motion(self, x, y, dx, dy): | |
pass | |
def on_mouse_scroll(self, x, y, mouse, direction): | |
pass | |
def on_text(self, text): | |
pass | |
def on_text_motion(self, motion): | |
pass | |
def on_text_motion_select(self, motion): | |
pass | |
class PushButton(WidgetBase): | |
"""Instance of a push button. | |
Triggers the event 'on_press' when it is clicked by the mouse. | |
Triggers the event 'on_release' when the mouse is released. | |
""" | |
def __init__(self, x, y, pressed, depressed, hover=None, batch=None, group=None): | |
"""Create a push button. | |
:Parameters: | |
`x` : int | |
X coordinate of the push button. | |
`y` : int | |
Y coordinate of the push button. | |
`pressed` : `~pyglet.image.AbstractImage` | |
Image to display when the button is pressed. | |
`depresseed` : `~pyglet.image.AbstractImage` | |
Image to display when the button isn't pressed. | |
`hover` : `~pyglet.image.AbstractImage` | |
Image to display when the button is being hovered over. | |
`batch` : `~pyglet.graphics.Batch` | |
Optional batch to add the push button to. | |
`group` : `~pyglet.graphics.Group` | |
Optional parent group of the push button. | |
""" | |
super().__init__(x, y, depressed.width, depressed.height) | |
self._pressed_img = pressed | |
self._depressed_img = depressed | |
self._hover_img = hover or depressed | |
# TODO: add `draw` method or make Batch required. | |
self._batch = batch or pyglet.graphics.Batch() | |
self._user_group = group | |
bg_group = Group(order=0, parent=group) | |
self._sprite = pyglet.sprite.Sprite(self._depressed_img, x, y, batch=batch, group=bg_group) | |
self._pressed = False | |
def _update_position(self): | |
self._sprite.position = self._x, self._y, 0 | |
def value(self): | |
return self._pressed | |
def value(self, value): | |
assert type(value) is bool, "This Widget's value must be True or False." | |
self._pressed = value | |
self._sprite.image = self._pressed_img if self._pressed else self._depressed_img | |
def update_groups(self, order): | |
self._sprite.group = Group(order=order + 1, parent=self._user_group) | |
def on_mouse_press(self, x, y, buttons, modifiers): | |
if not self.enabled or not self._check_hit(x, y): | |
return | |
self._sprite.image = self._pressed_img | |
self._pressed = True | |
self.dispatch_event('on_press') | |
def on_mouse_release(self, x, y, buttons, modifiers): | |
if not self.enabled or not self._pressed: | |
return | |
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img | |
self._pressed = False | |
self.dispatch_event('on_release') | |
def on_mouse_motion(self, x, y, dx, dy): | |
if not self.enabled or self._pressed: | |
return | |
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img | |
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): | |
if not self.enabled or self._pressed: | |
return | |
self._sprite.image = self._hover_img if self._check_hit(x, y) else self._depressed_img | |
PushButton.register_event_type('on_press') | |
PushButton.register_event_type('on_release') | |
class ToggleButton(PushButton): | |
"""Instance of a toggle button. | |
Triggers the event 'on_toggle' when the mouse is pressed or released. | |
""" | |
def _get_release_image(self, x, y): | |
return self._hover_img if self._check_hit(x, y) else self._depressed_img | |
def on_mouse_press(self, x, y, buttons, modifiers): | |
if not self.enabled or not self._check_hit(x, y): | |
return | |
self._pressed = not self._pressed | |
self._sprite.image = self._pressed_img if self._pressed else self._get_release_image(x, y) | |
self.dispatch_event('on_toggle', self._pressed) | |
def on_mouse_release(self, x, y, buttons, modifiers): | |
if not self.enabled or self._pressed: | |
return | |
self._sprite.image = self._get_release_image(x, y) | |
ToggleButton.register_event_type('on_toggle') | |
class Slider(WidgetBase): | |
"""Instance of a slider made of a base and a knob image. | |
Triggers the event 'on_change' when the knob position is changed. | |
The knob position can be changed by dragging with the mouse, or | |
scrolling the mouse wheel. | |
""" | |
def __init__(self, x, y, base, knob, edge=0, batch=None, group=None): | |
"""Create a slider. | |
:Parameters: | |
`x` : int | |
X coordinate of the slider. | |
`y` : int | |
Y coordinate of the slider. | |
`base` : `~pyglet.image.AbstractImage` | |
Image to display as the background to the slider. | |
`knob` : `~pyglet.image.AbstractImage` | |
Knob that moves to show the position of the slider. | |
`edge` : int | |
Pixels from the maximum and minimum position of the slider, | |
to the edge of the base image. | |
`batch` : `~pyglet.graphics.Batch` | |
Optional batch to add the slider to. | |
`group` : `~pyglet.graphics.Group` | |
Optional parent group of the slider. | |
""" | |
super().__init__(x, y, base.width, knob.height) | |
self._edge = edge | |
self._base_img = base | |
self._knob_img = knob | |
self._half_knob_width = knob.width / 2 | |
self._half_knob_height = knob.height / 2 | |
self._knob_img.anchor_y = knob.height / 2 | |
self._min_knob_x = x + edge | |
self._max_knob_x = x + base.width - knob.width - edge | |
self._user_group = group | |
bg_group = Group(order=0, parent=group) | |
fg_group = Group(order=1, parent=group) | |
self._base_spr = pyglet.sprite.Sprite(self._base_img, x, y, batch=batch, group=bg_group) | |
self._knob_spr = pyglet.sprite.Sprite(self._knob_img, x+edge, y+base.height/2, batch=batch, group=fg_group) | |
self._value = 0 | |
self._in_update = False | |
def _update_position(self): | |
self._base_spr.position = self._x, self._y, 0 | |
self._knob_spr.position = self._x + self._edge, self._y + self._base_img.height / 2, 0 | |
def value(self): | |
return self._value | |
def value(self, value): | |
assert type(value) in (int, float), "This Widget's value must be an int or float." | |
self._value = value | |
x = (self._max_knob_x - self._min_knob_x) * value / 100 + self._min_knob_x + self._half_knob_width | |
self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x)) | |
def update_groups(self, order): | |
self._base_spr.group = Group(order=order + 1, parent=self._user_group) | |
self._knob_spr.group = Group(order=order + 2, parent=self._user_group) | |
def _min_x(self): | |
return self._x + self._edge | |
def _max_x(self): | |
return self._x + self._width - self._edge | |
def _min_y(self): | |
return self._y - self._half_knob_height | |
def _max_y(self): | |
return self._y + self._half_knob_height + self._base_img.height / 2 | |
def _check_hit(self, x, y): | |
return self._min_x < x < self._max_x and self._min_y < y < self._max_y | |
def _update_knob(self, x): | |
self._knob_spr.x = max(self._min_knob_x, min(x - self._half_knob_width, self._max_knob_x)) | |
self._value = abs(((self._knob_spr.x - self._min_knob_x) * 100) / (self._min_knob_x - self._max_knob_x)) | |
self.dispatch_event('on_change', self._value) | |
def on_mouse_press(self, x, y, buttons, modifiers): | |
if not self.enabled: | |
return | |
if self._check_hit(x, y): | |
self._in_update = True | |
self._update_knob(x) | |
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): | |
if not self.enabled: | |
return | |
if self._in_update: | |
self._update_knob(x) | |
def on_mouse_scroll(self, x, y, mouse, direction): | |
if not self.enabled: | |
return | |
if self._check_hit(x, y): | |
self._update_knob(self._knob_spr.x + self._half_knob_width + direction) | |
def on_mouse_release(self, x, y, buttons, modifiers): | |
if not self.enabled: | |
return | |
self._in_update = False | |
Slider.register_event_type('on_change') | |
class TextEntry(WidgetBase): | |
"""Instance of a text entry widget. | |
Allows the user to enter and submit text. | |
""" | |
def __init__(self, text, x, y, width, | |
color=(255, 255, 255, 255), text_color=(0, 0, 0, 255), caret_color=(0, 0, 0), | |
batch=None, group=None): | |
"""Create a text entry widget. | |
:Parameters: | |
`text` : str | |
Initial text to display. | |
`x` : int | |
X coordinate of the text entry widget. | |
`y` : int | |
Y coordinate of the text entry widget. | |
`width` : int | |
The width of the text entry widget. | |
`color` : (int, int, int, int) | |
The color of the outline box in RGBA format. | |
`text_color` : (int, int, int, int) | |
The color of the text in RGBA format. | |
`caret_color` : (int, int, int) | |
The color of the caret in RGB format. | |
`batch` : `~pyglet.graphics.Batch` | |
Optional batch to add the text entry widget to. | |
`group` : `~pyglet.graphics.Group` | |
Optional parent group of text entry widget. | |
""" | |
self._doc = pyglet.text.document.UnformattedDocument(text) | |
self._doc.set_style(0, len(self._doc.text), dict(color=text_color)) | |
font = self._doc.get_font() | |
height = font.ascent - font.descent | |
self._user_group = group | |
bg_group = Group(order=0, parent=group) | |
fg_group = Group(order=1, parent=group) | |
# Rectangular outline with 2-pixel pad: | |
self._pad = p = 2 | |
self._outline = pyglet.shapes.Rectangle(x-p, y-p, width+p+p, height+p+p, color[:3], batch, bg_group) | |
self._outline.opacity = color[3] | |
# Text and Caret: | |
self._layout = IncrementalTextLayout(self._doc, width, height, multiline=False, batch=batch, group=fg_group) | |
self._layout.x = x | |
self._layout.y = y | |
self._caret = Caret(self._layout, color=caret_color) | |
self._caret.visible = False | |
self._focus = False | |
super().__init__(x, y, width, height) | |
def _update_position(self): | |
self._layout.position = self._x, self._y, 0 | |
self._outline.position = self._x - self._pad, self._y - self._pad | |
def value(self): | |
return self._doc.text | |
def value(self, value): | |
assert type(value) is str, "This Widget's value must be a string." | |
self._doc.text = value | |
def _check_hit(self, x, y): | |
return self._x < x < self._x + self._width and self._y < y < self._y + self._height | |
def _set_focus(self, value): | |
self._focus = value | |
self._caret.visible = value | |
def update_groups(self, order): | |
self._outline.group = Group(order=order + 1, parent=self._user_group) | |
self._layout.group = Group(order=order + 2, parent=self._user_group) | |
def on_mouse_motion(self, x, y, dx, dy): | |
if not self.enabled: | |
return | |
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): | |
if not self.enabled: | |
return | |
if self._focus: | |
self._caret.on_mouse_drag(x, y, dx, dy, buttons, modifiers) | |
def on_mouse_press(self, x, y, buttons, modifiers): | |
if not self.enabled: | |
return | |
if self._check_hit(x, y): | |
self._set_focus(True) | |
self._caret.on_mouse_press(x, y, buttons, modifiers) | |
else: | |
self._set_focus(False) | |
def on_text(self, text): | |
if not self.enabled: | |
return | |
if self._focus: | |
# Commit on Enter/Return: | |
if text in ('\r', '\n'): | |
self.dispatch_event('on_commit', self._layout.document.text) | |
self._set_focus(False) | |
return | |
self._caret.on_text(text) | |
def on_text_motion(self, motion): | |
if not self.enabled: | |
return | |
if self._focus: | |
self._caret.on_text_motion(motion) | |
def on_text_motion_select(self, motion): | |
if not self.enabled: | |
return | |
if self._focus: | |
self._caret.on_text_motion_select(motion) | |
def on_commit(self, text): | |
if not self.enabled: | |
return | |
"""Text has been commited via Enter/Return key.""" | |
TextEntry.register_event_type('on_commit') | |