import pygame
from tessella.constraints import WidgetConstraints
from . import SingleChildWidget
from . import Widget
[docs]
class Positioned(SingleChildWidget):
"""
A widget capable of manually positioning its children in regards to the edges of the space available, usually inside a `Stack` widget.
Attributes:
left (int | None): The left side offset, in pixels.
right (int | None): The right side offset, in pixels.
top (int | None): The top side offset, in pixels.
bottom (int | None): The bottom side offset, in pixels.
NOTE: `left` and `right` cannot be both set at once. The same applies for `top` and `bottom`.
"""
def __init__(
self,
child: Widget,
left: int | None=None,
right: int | None=None,
top: int | None=None,
bottom: int | None=None,
):
"""
Initializes a new Positioned object.
Args:
child (Widget): The child of this widget.
left (int | None): The left side offset, in pixels. If both \
`left` and `right` are None, defaults to 0.
right (int | None): The right side offset, in pixels. Defaults to None.
top (int | None): The top side offset, in pixels. If both \
`top` and `bottom` are None, defaults to 0.
bottom (int | None): The bottom side offset, in pixels. Defaults to None.
Raises:
AssertionError: If both `left` and `right` are set, or if \
both `top` and `bottom` are set.
"""
assert (not (left != None and right != None)), "Positioned widget cannot have both 'left' and 'right' properties at the same time."
assert (not (top != None and bottom != None)), "Positioned widget cannot have both 'top' and 'bottom' properties at the same time."
super().__init__(child)
# Aligns to topleft by default
if (left is None and right is None):
left = 0
if (top is None and bottom is None):
top = 0
self.left: int | None = left
self.right: int | None = right
self.top: int | None = top
self.bottom: int | None = bottom
def _align_child(self) -> None:
self.child.set_position(self.bounds.topleft)
[docs]
def set_position(self, new_position: tuple[int, int]) -> None:
"""
Ignores `new_position` and instead derives this widget's \
position from its parent's bounds and its `left`/`right`/`top`/ \
`bottom` offsets, so that it stays correctly positioned \
regardless of layout changes elsewhere in the tree. Also \
repositions the child accordingly.
Args:
new_position (tuple[int, int]): Unused; kept for signature \
compatibility with `Widget.set_position`.
"""
# This override is meant to correctly work out
# the position of this widget regardless of
# external conditions
if self.parent is not None:
parent_bounds = self.parent.get_bounds()
else:
parent_bounds = self.bounds
bounds = self.get_bounds()
if self.left is not None:
bounds.left = parent_bounds.left + self.left
else:
bounds.right = parent_bounds.right - self.right
if self.top is not None:
bounds.top = parent_bounds.top + self.top
else:
bounds.bottom = parent_bounds.bottom - self.bottom
# Ensures we also update the child position
super().set_position(bounds.topleft)
def _debug_draw(self) -> pygame.Surface | None:
surface = pygame.Surface(self.bounds.size, pygame.SRCALPHA)
surface_rect = surface.get_rect()
pygame.draw.rect(surface, "#C9E83D", surface_rect, 1)
return surface
def _draw(self) -> pygame.Surface | None:
return None
[docs]
def calculate_size(self, constraints: WidgetConstraints) -> tuple[int, int]:
"""
Shrinks the available constraints by whichever horizontal and \
vertical offsets are set, then lays out the child within what's \
left.
Args:
constraints (WidgetConstraints): The constraints to be used for \
determining the sizing of this widget.
Returns:
tuple[int, int]: The calculated size for this widget.
"""
if self.left != None:
constraints.max_width -= self.left
else:
constraints.max_width -= self.right
if self.top != None:
constraints.max_height -= self.top
else:
constraints.max_height -= self.bottom
return self.child.apply_layout_constraints(constraints)
[docs]
def depends_on_child_size(self) -> bool:
return True
[docs]
def depends_on_parent_size(self) -> bool:
return True