from typing import Iterable, TypeVar, Tuple
from arcade.gui.property import bind
from arcade.gui.widgets import UIWidget, UILayout
W = TypeVar("W", bound="UIWidget")
[docs]class UIAnchorLayout(UILayout):
"""
Places children based on anchor values.
Defaults to `size_hint = (1, 1)`.
Supports `size_hint`, `size_hint_min`, and `size_hint_max`.
Children may overlap.
Child are resized based on size_hint. Max and Min size_hints only take effect if a size_hint is given.
Allowed keyword options for `UIAnchorLayout.add()`
- anchor_x: str = None - uses `self.default_anchor_x` as default
- align_x: float = 0
- anchor_y: str = None - uses `self.default_anchor_y` as default
- align_y: float = 0
"""
default_anchor_x = "center"
default_anchor_y = "center"
def __init__(
self,
x: float = 0,
y: float = 0,
width: float = 100,
height: float = 100,
children: Iterable["UIWidget"] = tuple(),
size_hint=(1, 1),
size_hint_min=None,
size_hint_max=None,
style=None,
**kwargs
):
super().__init__(
x,
y,
width,
height,
children,
size_hint,
size_hint_min,
size_hint_max,
style,
**kwargs
)
def do_layout(self):
for child, data in self._children:
self._place_child(child, **data)
def add(
self,
child: W,
*,
anchor_x: str = None,
align_x: float = 0,
anchor_y: str = None,
align_y: float = 0,
**kwargs
) -> W:
return super(UIAnchorLayout, self).add(
child=child,
anchor_x=anchor_x,
align_x=align_x,
anchor_y=anchor_y,
align_y=align_y,
**kwargs
)
def _place_child(
self,
child: UIWidget,
anchor_x: str = None,
align_x: float = 0,
anchor_y: str = None,
align_y: float = 0,
):
anchor_x = anchor_x or self.default_anchor_x
anchor_y = anchor_y or self.default_anchor_y
# Handle size_hints
new_child_rect = child.rect
sh_w, sh_h = child.size_hint or (None, None)
shmn_w, shmn_h = child.size_hint_min or (None, None)
shmx_w, shmx_h = child.size_hint_max or (None, None)
if sh_w is not None:
new_child_rect = new_child_rect.resize(width=self.content_width * sh_w)
if shmn_w:
new_child_rect = new_child_rect.min_size(width=shmn_w)
if shmx_w:
new_child_rect = new_child_rect.max_size(width=shmx_w)
if sh_h is not None:
new_child_rect = new_child_rect.resize(height=self.content_height * sh_w)
if shmn_h:
new_child_rect = new_child_rect.min_size(height=shmn_h)
if shmx_h:
new_child_rect = new_child_rect.max_size(height=shmx_h)
# stay in bounds
new_child_rect = new_child_rect.max_size(*self.content_size)
# calculate position
content_rect = self.content_rect
anchor_x = "center_x" if anchor_x == "center" else anchor_x
child_anchor_x_value = getattr(new_child_rect, anchor_x)
own_anchor_x_value = getattr(content_rect, anchor_x)
diff_x = own_anchor_x_value + align_x - child_anchor_x_value
anchor_y = "center_y" if anchor_y == "center" else anchor_y
child_anchor_y_value = getattr(new_child_rect, anchor_y)
own_anchor_y_value = getattr(content_rect, anchor_y)
diff_y = own_anchor_y_value + align_y - child_anchor_y_value
# check if changes are required
if diff_x or diff_y or child.rect != new_child_rect:
child.rect = new_child_rect.move(diff_x, diff_y)
[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.
Hint: UIBoxLayout does not adjust its own size if children are added.
This requires a UIManager or UIAnchorLayout as parent.
Use `self.fit_content()` to resize, bottom-left is used as anchor point.
UIBoxLayout supports: size_hint, size_hint_min, size_hint_max
If a child widget provides a size_hint for a dimension, the child will be resized within the given range of
size_hint_min and size_hint_max (unrestricted if not given).
For vertical=True any available space (layout size - min_size of children) will be distributed to the child widgets
based on their size_hint.
: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 (default 0,0 -> minimal size to contain children)
: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,
width=0,
height=0,
vertical=True,
align="center",
children: Iterable[UIWidget] = tuple(),
size_hint=(0, 0),
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=width,
height=height,
children=children,
size_hint=size_hint,
size_hint_min=size_hint_min,
size_hint_max=size_hint_max,
style=style,
**kwargs
)
bind(self, "_children", self._update_size_hints)
# initially update size hints
self._update_size_hints()
@staticmethod
def _layouting_allowed(child: UIWidget) -> Tuple[bool, bool]:
"""
Checks if size_hint is given for the dimension, which would allow the layout to resize this widget
:return: horizontal, vertical
"""
sh_w, sh_h = child.size_hint or (None, None)
return sh_w is not None, sh_h is not None
def _update_size_hints(self):
required_space_between = max(0, len(self.children) - 1) * self._space_between
def min_size(child: UIWidget) -> Tuple[float, float]:
"""
Determine min size of a child widget
This can be the size_hint_min. If no size_hints are provided the child size has to stay the same and
the minimal size is the current size.
"""
h_allowed, v_allowed = UIBoxLayout._layouting_allowed(child)
shmn_w, shmn_h = child.size_hint_min or (None, None)
shmn_w = shmn_w or 0 if h_allowed else child.width
shmn_h = shmn_h or 0 if v_allowed else child.height
return shmn_w, shmn_h
min_child_sizes = [min_size(child) for child in self.children]
if len(self.children) == 0:
width = 0
height = 0
elif self.vertical:
width = max(size[0] for size in min_child_sizes)
height_of_children = sum(size[1] for size in min_child_sizes)
height = height_of_children + required_space_between
else:
width_of_children = sum(size[0] for size in min_child_sizes)
width = width_of_children + required_space_between
height = max(size[1] for size in min_child_sizes)
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.size_hint_min = base_width + width, base_height + height
[docs] def fit_content(self):
"""
Resize to fit content, using `self.size_hint_min`
:return: self
"""
self.rect = self.rect.resize(*self.size_hint_min)
return self
def do_layout(self):
start_y = self.content_rect.top
start_x = self.content_rect.left
if not self.children:
return
if self.vertical:
available_width = self.content_width
# calculate if some space is available for children to grow
available_height = max(0, self.height - self.size_hint_min[1])
total_size_hint_height = sum(
child.size_hint[1] or 0 for child in self.children if child.size_hint
) or 1 # prevent division by zero
for child in self.children:
new_rect = child.rect
# collect all size hints
sh_w, sh_h = child.size_hint or (None, None)
shmn_w, shmn_h = child.size_hint_min or (None, None)
shmx_w, shmx_h = child.size_hint_max or (None, None)
# apply y-axis
if sh_h is not None:
min_height_value = shmn_h or 0
# Maximal growth to parent.width * shw
available_growth_height = min_height_value + available_height * (
sh_h / total_size_hint_height
)
max_growth_height = self.height * sh_h
new_rect = new_rect.resize(
height=min(available_growth_height, max_growth_height)
)
if shmn_h is not None:
new_rect = new_rect.min_size(height=shmn_h)
if shmx_h is not None:
new_rect = new_rect.max_size(height=shmx_h)
# apply x-axis
if sh_w is not None:
new_rect = new_rect.resize(
width=max(available_width * sh_w, shmn_w or 0)
)
if shmn_w is not None:
new_rect = new_rect.min_size(width=shmn_w)
if shmx_w is not None:
new_rect = new_rect.max_size(width=shmx_w)
# align
if self.align == "left":
new_rect = new_rect.align_left(start_x)
elif self.align == "right":
new_rect = new_rect.align_right(start_x + self.content_width)
else:
center_x = start_x + self.content_width // 2
new_rect = new_rect.align_center_x(center_x)
new_rect = new_rect.align_top(start_y)
child.rect = new_rect
start_y -= child.height
start_y -= self._space_between
else:
center_y = start_y - self.content_height // 2
available_height = self.content_height
# calculate if some space is available for children to grow
available_width = max(0, self.width - self.size_hint_min[0])
total_size_hint_width = sum(
child.size_hint[0] or 0 for child in self.children if child.size_hint
) or 1 # prevent division by zero
# TODO Fix layout algorithm, handle size hints per dimension!
# 0. check if any hint given, if not, continue with step 4.
# 1. change size to minimal
# 2. grow using size_hint
# 3. ensure size_hint_max
# 4. place child
for child in self.children:
new_rect = child.rect
# collect all size hints
sh_w, sh_h = child.size_hint or (None, None)
shmn_w, shmn_h = child.size_hint_min or (None, None)
shmx_w, shmx_h = child.size_hint_max or (None, None)
# apply x-axis
if sh_w is not None:
min_width_value = shmn_w or 0
# new_rect = new_rect.resize(width=min_width_value) # TODO should not be required!
# Maximal growth to parent.width * shw
available_growth_width = min_width_value + available_width * (
sh_w / total_size_hint_width
)
max_growth_width = self.width * sh_w
new_rect = new_rect.resize(
width=min(
available_growth_width, max_growth_width
) # this does not enforce min width
)
if shmn_w is not None:
new_rect = new_rect.min_size(width=shmn_w)
if shmx_w is not None:
new_rect = new_rect.max_size(width=shmx_w)
# apply y-axis
if sh_h is not None:
new_rect = new_rect.resize(
height=max(available_height * sh_h, shmn_h or 0)
)
if shmn_h is not None:
new_rect = new_rect.min_size(height=shmn_h)
if shmx_h is not None:
new_rect = new_rect.max_size(height=shmx_h)
# align
if self.align == "top":
new_rect = new_rect.align_top(start_y)
elif self.align == "bottom":
new_rect = new_rect.align_bottom(start_y - self.content_height)
else:
new_rect = new_rect.align_center_y(center_y)
new_rect = new_rect.align_left(start_x)
child.rect = new_rect
start_x += child.width
start_x += self._space_between
[docs]class UIGridLayout(UILayout):
"""
Places widget in a grid layout.
:param float x: x coordinate of bottom left
:param float y: y coordinate of bottom left
:param float align_horizontal: Align children in orthogonal direction (x: left, center, right)
:param float align_vertical: Align children in orthogonal direction (y: top, center, bottom)
:param Iterable[UIWidget] 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 horizontal_spacing: Space between columns
:param vertical_spacing: Space between rows
:param int column_count: Number of columns in the grid, can be changed
:param int row_count: Number of rows in the grid, can be changed
"""
def __init__(
self,
x=0,
y=0,
align_horizontal="center",
align_vertical="center",
children: Iterable[UIWidget] = tuple(),
size_hint=None,
size_hint_min=None,
size_hint_max=None,
horizontal_spacing: int = 0,
vertical_spacing: int = 0,
column_count: int = 1,
row_count: int = 1,
style=None,
**kwargs
):
super(UIGridLayout, self).__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
)
self._horizontal_spacing = horizontal_spacing
self._vertical_spacing = vertical_spacing
self.column_count = column_count
self.row_count = row_count
self.align_horizontal = align_horizontal
self.align_vertical = align_vertical
bind(self, "_children", self._update_size_hints)
# initially update size hints
self._update_size_hints()
def _update_size_hints(self):
child_sorted_row_wise = [
[None for _ in range(self.column_count)] for _ in range(self.row_count)
]
max_width_per_column = [
[(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count)
]
max_height_per_row = [
[(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count)
]
for child, data in self._children:
col_num = data["col_num"]
row_num = data["row_num"]
col_span = data["col_span"]
row_span = data["row_span"]
for i in range(col_num, col_span + col_num):
max_width_per_column[i][row_num] = (0, 0)
max_width_per_column[col_num][row_num] = (child.width, col_span)
for i in range(row_num, row_span + row_num):
max_height_per_row[i][col_num] = (0, 0)
max_height_per_row[row_num][col_num] = (child.height, row_span)
for row in child_sorted_row_wise[row_num: row_num + row_span]:
row[col_num: col_num + col_span] = [child] * col_span
principal_width_ratio_list = []
principal_height_ratio_list = []
for row in max_height_per_row:
principal_height_ratio_list.append(
max(height / (span or 1) for height, span in row)
)
for col in max_width_per_column:
principal_width_ratio_list.append(
max(width / (span or 1) for width, span in col)
)
base_width = self.padding_left + self.padding_right + 2 * self.border_width
base_height = self.padding_top + self.padding_bottom + 2 * self.border_width
content_height = (
sum(principal_height_ratio_list) + self.row_count * self._vertical_spacing
)
content_width = (
sum(principal_width_ratio_list)
+ self.column_count * self._horizontal_spacing
)
self.size_hint_min = (base_width + content_width, base_height + content_height)
[docs] def add(
self,
child: W,
col_num: int = 0,
row_num: int = 0,
col_span: int = 1,
row_span: int = 1,
**kwargs
) -> W:
"""
Adds widgets in the grid.
:param UIWidget child: The widget which is to be added in the grid
:param int col_num: The column number in which the widget is to be added (first column is numbered 0; left)
:param int row_num: The row number in which the widget is to be added (first row is numbered 0; top)
:param int col_span: Number of columns the widget will stretch for.
:param int row_span: Number of rows the widget will stretch for.
"""
return super().add(
child,
col_num=col_num,
row_num=row_num,
col_span=col_span,
row_span=row_span,
**kwargs
)
def do_layout(self):
initial_left_x = self.content_rect.left
start_y = self.content_rect.top
if not self.children:
return
child_sorted_row_wise = [
[None for _ in range(self.column_count)] for _ in range(self.row_count)
]
max_width_per_column = [
[(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count)
]
max_height_per_row = [
[(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count)
]
for child, data in self._children:
col_num = data["col_num"]
row_num = data["row_num"]
col_span = data["col_span"]
row_span = data["row_span"]
for i in range(col_num, col_span + col_num):
max_width_per_column[i][row_num] = (0, 0)
max_width_per_column[col_num][row_num] = (child.width, col_span)
for i in range(row_num, row_span + row_num):
max_height_per_row[i][col_num] = (0, 0)
max_height_per_row[row_num][col_num] = (child.height, row_span)
for row in child_sorted_row_wise[row_num: row_num + row_span]:
row[col_num: col_num + col_span] = [child] * col_span
# making max_height_per_row and max_width_per_column uniform
for row in max_height_per_row:
principal_height_ratio = max(height / (span or 1) for height, span in row)
for i, (height, span) in enumerate(row):
if height / (span or 1) < principal_height_ratio:
row[i] = (principal_height_ratio * span, span)
for col in max_width_per_column:
principal_width_ratio = max(width / (span or 1) for width, span in col)
for i, (width, span) in enumerate(col):
if width / (span or 1) < principal_width_ratio:
col[i] = (principal_width_ratio * span, span)
# row wise rendering children
for row_num, row in enumerate(child_sorted_row_wise):
max_height_row = 0
start_x = initial_left_x
for col_num, child in enumerate(row):
max_height = (
max_height_per_row[row_num][col_num][0] + self._vertical_spacing
)
max_width = (
max_width_per_column[col_num][row_num][0] + self._horizontal_spacing
)
if max_width == self._horizontal_spacing:
max_width = 0
if max_height == self._vertical_spacing:
max_height = 0
col_span = max_width_per_column[col_num][row_num][1] or 1
row_span = max_height_per_row[row_num][col_num][1] or 1
center_y = start_y - (max_height / 2)
center_x = start_x + (max_width / 2)
start_x += max_width
if max_height / row_span > max_height_row:
max_height_row = max_height / row_span
if child is not None and max_width != 0 and max_height != 0:
if self.align_vertical == "top":
new_rect = child.rect.align_top(start_y)
elif self.align_vertical == "bottom":
new_rect = child.rect.align_bottom(start_y - max_height)
else:
new_rect = child.rect.align_center_y(center_y)
if self.align_horizontal == "left":
new_rect = new_rect.align_left(start_x - max_width)
elif self.align_horizontal == "right":
new_rect = new_rect.align_right(start_x)
else:
new_rect = new_rect.align_center_x(center_x)
if new_rect != child.rect:
child.rect = new_rect
start_y -= max_height_row