from abc import abstractmethod, ABC
from random import randint
from typing import NamedTuple, Iterable, Optional, List, Union, TYPE_CHECKING, TypeVar
import pyglet
from pyglet.event import EventDispatcher, EVENT_HANDLED, EVENT_UNHANDLED
from pyglet.text.caret import Caret
from pyglet.text.document import AbstractDocument
import arcade
from arcade import Texture, Sprite
from arcade.gui.events import UIEvent, UIMouseMovementEvent, UIMousePressEvent, UIMouseReleaseEvent, \
UITextEvent, \
UIMouseDragEvent, \
UIMouseScrollEvent, UITextMotionEvent, UITextMotionSelectEvent, UIMouseEvent, UIOnClickEvent, UIOnUpdateEvent
from arcade.gui.surface import Surface
if TYPE_CHECKING:
from arcade.gui.ui_manager import UIManager
class _Rect(NamedTuple):
x: float
y: float
width: float
height: float
def move(self, dx: float = 0, dy: float = 0):
"""Returns new Rect which is moved by dx and dy"""
return _Rect(self.x + dx, self.y + dy, self.width, self.height)
def collide_with_point(self, x, y):
left, bottom, width, height = self
return left < x < left + width and bottom < y < bottom + height
def scale(self, scale: float) -> "_Rect":
"""Returns a new rect with scale applied"""
return _Rect(
int(self.x * scale),
int(self.y * scale),
int(self.width * scale),
int(self.height * scale),
)
def resize(self, width=None, height=None):
width = width or self.width
height = height or self.height
return _Rect(self.x, self.y, width, height)
@property
def size(self):
return self.width, self.height
@property
def left(self):
return self.x
@property
def right(self):
return self.x + self.width
@property
def bottom(self):
return self.y
@property
def top(self):
return self.y + self.height
@property
def center_x(self):
return self.x + self.width / 2
@property
def center_y(self):
return self.y + self.height / 2
@property
def center(self):
return self.left, self.bottom
@property
def position(self):
"""Bottom left coordinates"""
return self.left, self.bottom
def align_top(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the top"""
diff_y = value - self.top
return self.move(dy=diff_y)
def align_bottom(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the bottom"""
diff_y = value - self.bottom
return self.move(dy=diff_y)
def align_left(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the left"""
diff_x = value - self.left
return self.move(dx=diff_x)
def align_right(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the right"""
diff_x = value - self.right
return self.move(dx=diff_x)
def align_center(self, center_x, center_y):
"""Returns new Rect, which is aligned to the center x and y"""
diff_x = center_x - self.center_x
diff_y = center_y - self.center_y
return self.move(dx=diff_x, dy=diff_y)
def align_center_x(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the center_x"""
diff_x = value - self.center_x
return self.move(dx=diff_x)
def align_center_y(self, value: float) -> "_Rect":
"""Returns new Rect, which is aligned to the center_y"""
diff_y = value - self.center_y
return self.move(dy=diff_y)
W = TypeVar('W', bound="UIWidget")
[docs]class UIDummy(UIInteractiveWidget):
"""
Solid color widget, used for testing.
:param float x: x coordinate of bottom left
:param float y: y coordinate of bottom left
:param color: fill color for the widget
:param width: width of widget
:param height: height of widget
: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=0, y=0, width=100, height=100, color=arcade.color.BLACK,
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)
self.color = color
self.frame = randint(0, 255)
def do_render(self, surface: Surface):
self.prepare_render(surface)
self.frame += 1
frame = self.frame % 256
surface.clear((self.color[0], self.color[1], self.color[2], frame))
if self.hovered:
arcade.draw_xywh_rectangle_outline(0, 0,
self.width, self.height,
color=arcade.color.BATTLESHIP_GREY,
border_width=3)
[docs]class UITextureButton(UIInteractiveWidget):
"""
A button with an image for the face of the button.
:param float x: x coordinate of bottom left
:param float y: y coordinate of bottom left
:param float width: width of widget. Defaults to texture width if not specified.
:param float height: height of widget. Defaults to texture height if not specified.
:param Texture texture: texture to display for the widget.
:param Texture texture_hovered: different texture to display if mouse is hovering over button.
:param Texture texture_pressed: different texture to display if mouse button is pressed while hovering over button.
:param str text: text to add to the button.
:param style: style information for the button.
:param float scale: scale the button, based on the base texture size.
: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
"""
def __init__(self,
x: float = 0,
y: float = 0,
width: float = None,
height: float = None,
texture: Texture = None,
texture_hovered: Texture = None,
texture_pressed: Texture = None,
text: str = "",
scale: float = None,
size_hint=None,
size_hint_min=None,
size_hint_max=None,
style=None,
**kwargs):
if width is None and texture is not None:
width = texture.width
if height is None and texture is not None:
height = texture.height
if scale is not None and texture is not None:
height = texture.height * scale
width = texture.width * scale
super().__init__(x, y, width, height,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max)
self._tex = texture
self._tex_hovered = texture_hovered
self._tex_pressed = texture_pressed
self._style = style or {}
self._text = text
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = value
self.trigger_render()
@property
def texture(self):
return self._tex
@texture.setter
def texture(self, value: Texture):
self._tex = value
self.trigger_render()
@property
def texture_hovered(self):
return self._tex_hovered
@texture_hovered.setter
def texture_hovered(self, value: Texture):
self._tex_hovered = value
self.trigger_render()
@property
def texture_pressed(self):
return self._tex_pressed
@texture_pressed.setter
def texture_pressed(self, value: Texture):
self._tex_pressed = value
self.trigger_render()
def do_render(self, surface: Surface):
self.prepare_render(surface)
tex = self._tex
if self.pressed and self._tex_pressed:
tex = self._tex_pressed
elif self.hovered and self._tex_hovered:
tex = self._tex_hovered
if tex:
surface.draw_texture(0, 0, self.width, self.height, tex)
if self.text:
text_margin = 2
font_size = self._style.get("font_size", 15)
font_color = self._style.get("font_color", arcade.color.WHITE)
border_width = self._style.get("border_width", 2)
# border_color = self._style.get("border_color", None)
# bg_color = self._style.get("bg_color", (21, 19, 21))
start_x = self.width // 2
start_y = self.height // 2 + 4
if self.pressed:
start_y -= 2
arcade.draw_text(
text=self.text,
start_x=start_x,
start_y=start_y,
font_size=font_size,
color=font_color,
align="center",
anchor_x='center', anchor_y='center',
width=self.width - 2 * border_width - 2 * text_margin
)
[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.label = 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,
)
if not height:
height = self.label.content_height
if not width:
width = self.label.content_width
super().__init__(x, y, width, height, # type: ignore
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max)
self.label.width = width
self.label.height = height
[docs] def fit_content(self):
"""
Sets the width and height of this UIWidget to contain the whole text.
"""
self.rect = self.x, self.y, self.label.content_width, self.label.content_height
@property
def text(self):
return self.label.text
@text.setter
def text(self, value):
self.label.text = value
self.trigger_full_render()
@property
def rect(self) -> _Rect:
return self._rect
@rect.setter
def rect(self, value):
self._rect = _Rect(*value)
self.trigger_full_render()
# Update Pyglet layout
label = self.label
label.begin_update()
label.x, label.y, label.width, label.height = 0, 0, self.width, self.height
label.end_update()
def do_render(self, surface: Surface):
self.prepare_render(surface)
with surface.ctx.pyglet_rendering():
self.label.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.width,
height=self.height,
multiline=multiline,
)
[docs] def fit_content(self):
"""
Sets the width and height of this UIWidget to contain the whole text.
"""
self.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_full_render()
@property
def rect(self) -> _Rect:
return self._rect
@rect.setter
def rect(self, value):
self._rect = _Rect(*value)
self.trigger_full_render()
# Update Pyglet layout
my_layout = self.layout
my_layout.begin_update()
my_layout.x, my_layout.y, my_layout.width, my_layout.height = 0, 0, self.width, self.height
my_layout.end_update()
def do_render(self, surface: Surface):
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
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 = _Arcade_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_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, 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
@property
def rect(self) -> _Rect:
return self._rect
@rect.setter
def rect(self, value):
self._rect = _Rect(*value)
self.trigger_full_render()
# Update Pyglet layout
my_layout = self.layout
my_layout.begin_update()
my_layout.x, my_layout.y, my_layout.width, my_layout.height = 0, 0, self.width, self.height
my_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.prepare_render(surface)
with surface.ctx.pyglet_rendering():
self.layout.draw()
[docs]class UILayout(UIWidget, UIWidgetParent):
"""
Base class for widgets, which position themselves or their children.
: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 children: Child widgets of this group
:param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` would like to grow
: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=0, y=0, width=100, height=100,
children: Iterable[UIWidget] = tuple(),
size_hint=None,
size_hint_min=None,
size_hint_max=None,
style=None,
**kwargs):
super().__init__(x, y, width, height,
children=children,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
style=style,
**kwargs)
def add(self, child: "UIWidget", **kwargs) -> "UIWidget":
super().add(child)
self.do_layout()
return child
def remove(self, child: "UIWidget"):
super().remove(child)
self.do_layout()
def clear(self):
super().clear()
self.do_layout()
[docs] def do_layout(self):
"""
Triggered by the UIManager before rendering, :class:`UILayout` s should place themselves and/or children.
Do layout will be triggered on children afterwards.
Use :meth:`UIWidget.trigger_render` to trigger a rendering before the next frame, this will happen automatically
if the position or size of this widget changed.
"""
def _do_layout(self):
# do layout, detect changed rect
rect = self.rect
self.do_layout()
if rect != self.rect:
self.trigger_render()
# Continue do_layout within subtree
super()._do_layout()
[docs]class UIWrapper(UILayout, UIWidgetParent):
"""
Wraps a :class:`arcade.gui.UIWidget` and reserves space around it, exactly one child supported.
:param child: Single child of this wrapper
:param padding: space around (top, right, bottom, left)
: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,
*,
child: UIWidget,
padding=(0, 0, 0, 0),
size_hint=None,
size_hint_min=None,
size_hint_max=None,
style=None,
):
"""
:param child: Child Widget which will be wrapped
:param padding: Space between top, right, bottom, left
"""
self._pad = padding
super().__init__(*child.rect,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
children=[child])
@property
def rect(self) -> _Rect:
# Adjust Rect to consume _pad more then child
x, y, w, h = self.child.rect
pt, pr, pb, pl = self._pad
return _Rect(x - pl, y - pb, w + pl + pr, h + pb + pt)
@rect.setter
def rect(self, value: _Rect):
# Child Rect has to be _pad smaller
x, y, w, h = value
pt, pr, pb, pl = self._pad
self.child.rect = _Rect(x + pl, y + pb, w - pl - pr, h - pb - pt)
@property
def child(self) -> UIWidget:
return self.children[0]
@child.setter
def child(self, value: UIWidget):
self.children[0] = value
[docs]class UISpace(UIWidget):
"""
Widget reserving space, can also have a background color.
: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 color: Color for widget area
: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=0, y=0, width=100, height=100, color=(0, 0, 0, 0),
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)
self._color = color
@property
def color(self):
return self._color
@color.setter
def color(self, value):
self._color = value
self.trigger_render()
def do_render(self, surface: Surface):
self.prepare_render(surface)
surface.clear(self._color)
[docs]class UIBorder(UIWrapper):
"""
Wraps a Widget with a border of given color.
:param child: Child of this wrapper
:param border_width: Width of the border
:param border_color: Color of the border
: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, child: UIWidget, border_width=2, border_color=(0, 0, 0, 255),
size_hint=None,
size_hint_min=None,
size_hint_max=None,
style=None,
**kwargs):
super().__init__(
child=child,
padding=(border_width, border_width, border_width, border_width),
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max
)
self._border_color = border_color
self._border_width = border_width
def do_render(self, surface: Surface):
self.prepare_render(surface)
arcade.draw_xywh_rectangle_outline(0, 0, self.width, self.height,
color=self._border_color,
border_width=self._border_width)
[docs]class UITexturePane(UIWrapper):
"""
This wrapper draws a background before child widget is rendered
:param child: Child of this wrapper
:param tex: Texture to use as background
:param padding: Space between the outer border of this widget and the child
: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, child: UIWidget,
tex: Texture,
padding=(0, 0, 0, 0),
size_hint=(1, 1),
size_hint_min=None,
size_hint_max=None,
style=None,
**kwargs):
super().__init__(
child=child,
padding=padding,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max
)
self._tex = tex
def do_render(self, surface: Surface):
self.prepare_render(surface)
surface.draw_texture(0, 0, self.width, self.height, tex=self._tex)
[docs]class UIPadding(UIWrapper):
"""Wraps a Widget and applies padding.
:param child: Child of this wrapper
:param bg_color: background color
:param padding: Space between the outer border of this widget and the child
: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, child: UIWidget,
padding=(0, 0, 0, 0),
bg_color=None,
size_hint=(1, 1),
size_hint_min=None,
size_hint_max=None,
**kwargs):
"""
:arg padding: Padding - top, right, bottom, left
:param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` would
like to grow (default: (1, 1) -> full size of parent)
"""
super().__init__(
child=child,
padding=padding,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max
)
self._bg_color = bg_color
def do_render(self, surface: Surface):
self.prepare_render(surface)
if self._bg_color:
# clear with bg color if set
surface.clear(self._bg_color)
[docs]class UIBoxLayout(UILayout):
"""
Places widgets next to each other.
Depending on the vertical attribute, the Widgets are placed top to bottom or left to right.
:param float x: x coordinate of bottom left
:param float y: y coordinate of bottom left
:param vertical: Layout children vertical (True) or horizontal (False)
:param align: Align children in orthogonal direction (x: left, center, right / y: top, center, bottom)
:param children: Initial children, more can be added
:param size_hint: A hint for :class:`UILayout`, if this :class:`UIWidget` would like to grow
:param size_hint_min: min width and height in pixel
:param size_hint_max: max width and height in pixel
:param space_between: Space between the children
"""
def __init__(self, x=0, y=0,
vertical=True,
align="center",
children: Iterable[UIWidget] = tuple(),
size_hint=None,
size_hint_min=None,
size_hint_max=None,
space_between=0,
style=None,
**kwargs):
self.align = align
self.vertical = vertical
self._space_between = space_between
super().__init__(x=x,
y=y,
width=0,
height=0,
children=children,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
style=style,
**kwargs)
def do_layout(self):
# TODO support self.align and self.spacing
initial_top = self.top
start_y = self.top
start_x = self.left
# min_height = sum(map(attrgetter("height"), self.children))
# min_width = sum(map(attrgetter("width"), self.children))
if not self.children:
self.rect = _Rect(self.left, self.bottom, 0, 0)
return
required_space_between = max(0, len(self.children) - 1) * self._space_between
if self.vertical:
new_height = sum(child.height for child in self.children) + required_space_between
new_width = max(child.width for child in self.children)
center_x = start_x + new_width // 2
for child in self.children:
if self.align == "left":
new_rect = child.rect.align_left(start_x)
elif self.align == "right":
new_rect = child.rect.align_right(start_x + new_width)
else:
new_rect = child.rect.align_center_x(center_x)
new_rect = new_rect.align_top(start_y)
if new_rect != child.rect:
child.rect = new_rect
start_y -= child.height
start_y -= self._space_between
else:
new_height = max(child.height for child in self.children)
new_width = sum(child.width for child in self.children) + required_space_between
center_y = start_y - new_height // 2
for child in self.children:
if self.align == "top":
new_rect = child.rect.align_top(start_y)
elif self.align == "bottom":
new_rect = child.rect.align_bottom(start_y - new_height)
else:
new_rect = child.rect.align_center_y(center_y)
new_rect = new_rect.align_left(start_x)
if new_rect != child.rect:
child.rect = new_rect
start_x += child.width
start_x += self._space_between
self._rect = _Rect(self.left, self.bottom, new_width, new_height).align_top(initial_top)