Skip to content

Commit

Permalink
feat: Make it easier to create just a BoxRow or BoxColumn without usi…
Browse files Browse the repository at this point in the history
…ng the grid factory function. (#98)
  • Loading branch information
MartinHowarth authored Apr 15, 2020
1 parent ad49595 commit d4437e1
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 69 deletions.
4 changes: 2 additions & 2 deletions examples/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pyglet.window import key

import cocos
from shimmer.components.box_layout import create_box_layout, BoxLayoutDefinition
from shimmer.components.box_layout import create_box_layout, BoxGridDefinition
from shimmer.components.font import FontDefinition
from shimmer.data_structures import White, Black
from shimmer.keyboard import (
Expand All @@ -28,7 +28,7 @@ class Calculator(Window):
["1", "2", "3", "/"],
["C", "0", "=", "*"],
]
layout_definition = BoxLayoutDefinition(num_columns=4, num_rows=4)
layout_definition = BoxGridDefinition(num_columns=4, num_rows=4)

def __init__(self):
"""Create a Calculator."""
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 120 additions & 37 deletions shimmer/components/box_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from abc import abstractmethod
from dataclasses import dataclass
from typing import List, Union, Optional, Type, Iterable
from typing import List, Union, Optional, Type, Iterable, Tuple

import cocos
from .box import Box, BoxDefinition
Expand All @@ -15,16 +15,47 @@


@dataclass(frozen=True)
class BoxLayoutDefinition(BoxDefinition):
class BoxRowDefinition(BoxDefinition):
"""
Definition of a layout of Boxes.
Definition of a row of Boxes.
:param spacing: Pixel spacing to leave between boxes.
:param alignment: Whether boxes should be aligned with each other at the top, center
or bottom. Only has an impact if the boxes are of different sizes.
"""

spacing: int = 10
alignment: VerticalAlignment = VerticalAlignment.center


@dataclass(frozen=True)
class BoxColumnDefinition(BoxDefinition):
"""
Definition of a column of Boxes.
:param spacing: Pixel spacing to leave between boxes.
:param alignment: Whether boxes should be aligned with each other on the left, center
or right. Only has an impact if the boxes are of different sizes.
"""

spacing: int = 10
alignment: HorizontalAlignment = HorizontalAlignment.center


@dataclass(frozen=True)
class BoxGridDefinition(BoxDefinition):
"""
Definition of a rectangular grid of Boxes.
Supported layouts are:
- vertically column
- horizontal row
- rectangular grid
- single vertical column
- single horizontal row
- rectangular grid with multiple rows/columns
:param spacing: Number of pixels to leave between Boxes.
Possible types:
- int: Pixel spacing in both x and y directions
- Tuple[int, int]: Pixel spacing in (x, y) directions respectively.
:param num_columns: If given, boxes will fill row-by-row upwards; up to a maximum of `height`.
:param num_rows: If given and `num_columns` is not, then boxes will fill
column-by-column from left to right with no max width.
Expand All @@ -37,20 +68,30 @@ class BoxLayoutDefinition(BoxDefinition):
in each row or column.
"""

spacing: int = 10
spacing: Union[int, Tuple[int, int]] = 10
num_columns: Optional[int] = None
num_rows: Optional[int] = 1
alignment: PositionalAnchor = CenterCenter

@property
def row_definition(self) -> BoxRowDefinition:
"""Definition for each row of the grid."""
spacing = self.spacing if isinstance(self.spacing, int) else self.spacing[0]
return BoxRowDefinition(spacing=spacing, alignment=self.alignment.vertical)

class BoxLayout(Box):
"""A collection of Boxes with a well defined layout."""
@property
def column_definition(self) -> BoxColumnDefinition:
"""Definition for each column of the grid."""
spacing = self.spacing if isinstance(self.spacing, int) else self.spacing[1]
return BoxColumnDefinition(spacing=spacing, alignment=self.alignment.horizontal)

definition_type: Type[BoxLayoutDefinition] = BoxLayoutDefinition

class BoxLayoutBase(Box):
"""A collection of Boxes with a well defined layout."""

def __init__(
self,
definition: Optional[BoxLayoutDefinition] = None,
definition: Optional[BoxDefinition] = None,
boxes: Optional[Iterable[Box]] = None,
):
"""
Expand All @@ -59,9 +100,8 @@ def __init__(
:param definition: Definition of this Layout.
:param boxes: List of Boxes to include in the layout.
"""
super(BoxLayout, self).__init__(definition)
super(BoxLayoutBase, self).__init__(definition)
self._boxes: List[Box] = []
self.definition: BoxLayoutDefinition = self.definition
for box in boxes or []:
self.add(box)
self.update_layout()
Expand All @@ -75,7 +115,7 @@ def remove(
:param obj: CocosNode to remove.
:param no_resize: If True, then the size of this box is not dynamically changed.
"""
super(BoxLayout, self).remove(obj, no_resize=no_resize)
super(BoxLayoutBase, self).remove(obj, no_resize=no_resize)
if isinstance(obj, Box):
self._boxes.remove(obj)
self.update_layout()
Expand All @@ -97,7 +137,7 @@ def add(
:param no_resize: Ignored.
:param position: Index to insert the box into the list of boxes. Defaults to the end.
"""
super(BoxLayout, self).add(child, z, name)
super(BoxLayoutBase, self).add(child, z, name)
if isinstance(child, Box):
if position is None:
self._boxes.append(child)
Expand All @@ -110,9 +150,25 @@ def update_layout(self) -> None:
"""Update the position of all boxes in this Layout."""


class BoxRow(BoxLayout):
class BoxRow(BoxLayoutBase):
"""Arranges boxes horizontally. Boxes are arranged from left to right."""

definition_type: Type[BoxRowDefinition] = BoxRowDefinition

def __init__(
self,
definition: Optional[BoxRowDefinition] = None,
boxes: Optional[Iterable[Box]] = None,
):
"""
Create a new BoxRow.
:param definition: Definition to use to layout the boxes.
:param boxes: The boxes to be laid out. More can be added later using `self.add(box)`.
"""
super(BoxRow, self).__init__(definition, boxes)
self.definition: BoxRowDefinition = self.definition

def update_layout(self) -> None:
"""Update the position of all boxes in this Layout."""
# Don't update layout if there are no boxes to layout.
Expand All @@ -126,24 +182,40 @@ def update_layout(self) -> None:
for box in self._boxes:
box.x = x_total
x_total += box.rect.width + self.definition.spacing
if self.definition.alignment.vertical == VerticalAlignment.bottom:
if self.definition.alignment == VerticalAlignment.bottom:
box.y = 0
elif self.definition.alignment.vertical == VerticalAlignment.center:
elif self.definition.alignment == VerticalAlignment.center:
box.y = (tallest / 2) - (box.rect.height / 2)
elif self.definition.alignment.vertical == VerticalAlignment.top:
elif self.definition.alignment == VerticalAlignment.top:
box.y = tallest - box.rect.height
else:
raise AssertionError(
f"{self.definition.alignment.vertical} "
f"{self.definition.alignment} "
f"must be a member of {VerticalAlignment}."
)

self.update_rect()


class BoxColumn(BoxLayout):
class BoxColumn(BoxLayoutBase):
"""Arranges boxes vertically. Boxes are arranged from bottom to top."""

definition_type: Type[BoxColumnDefinition] = BoxColumnDefinition

def __init__(
self,
definition: Optional[BoxColumnDefinition] = None,
boxes: Optional[Iterable[Box]] = None,
):
"""
Create a new BoxColumn.
:param definition: Definition to use to layout the boxes.
:param boxes: The boxes to be laid out. More can be added later using `self.add(box)`.
"""
super(BoxColumn, self).__init__(definition, boxes)
self.definition: BoxColumnDefinition = self.definition

def update_layout(self) -> None:
"""Update the position of all boxes in this Layout."""
# Don't update layout if there are no boxes to layout.
Expand All @@ -158,23 +230,23 @@ def update_layout(self) -> None:
box.y = y_total
y_total += box.rect.height + self.definition.spacing

if self.definition.alignment.horizontal == HorizontalAlignment.left:
if self.definition.alignment == HorizontalAlignment.left:
box.x = 0
elif self.definition.alignment.horizontal == HorizontalAlignment.center:
elif self.definition.alignment == HorizontalAlignment.center:
box.x = (widest / 2) - (box.rect.width / 2)
elif self.definition.alignment.horizontal == HorizontalAlignment.right:
elif self.definition.alignment == HorizontalAlignment.right:
box.x = widest - box.rect.width
else:
raise AssertionError(
f"{self.definition.alignment.horizontal} "
f"{self.definition.alignment} "
f"must be a member of {HorizontalAlignment}."
)

self.update_rect()


def build_rectangular_grid(
definition: BoxLayoutDefinition, boxes: List[Box]
definition: BoxGridDefinition, boxes: List[Box]
) -> Union[BoxRow, BoxColumn]:
"""
Build a rectangular grid of boxes.
Expand All @@ -187,27 +259,32 @@ def build_rectangular_grid(
This is passed to all of the layouts created, including the nested ones.
:param boxes: Boxes to include.
"""
grid_type: Union[Type[BoxRow], Type[BoxColumn]]
element_type: Union[Type[BoxRow], Type[BoxColumn]]
if definition.num_columns is not None:
grid_type = BoxColumn
element_type = BoxRow
boxes_per_element = definition.num_columns
max_elements = definition.num_rows
max_elements_per_line = definition.num_rows
elif definition.num_rows is not None:
grid_type = BoxRow
element_type = BoxColumn
boxes_per_element = definition.num_rows
max_elements = definition.num_columns
max_elements_per_line = definition.num_columns
else:
raise ValueError(
"Grid layout is undefined with both `num_columns` and `num_rows` undefined."
)

# Maximum elements is one per box
max_elements = max_elements if max_elements is not None else len(boxes)
# Maximum number of grid elements per row/column is one element
# per box to be placed in the grid.
max_elements_per_line = (
max_elements_per_line if max_elements_per_line is not None else len(boxes)
)
element_index = 0
elements = []
while True:
if element_index >= max_elements:
if element_index >= max_elements_per_line:
# Reached max requested num_columns/num_rows
break

Expand All @@ -220,26 +297,32 @@ def build_rectangular_grid(
# Last element might have fewer than other elements, but that's ok.
break

elements.append(element_type(definition, box_batch))
if element_type is BoxRow:
elements.append(BoxRow(definition.row_definition, box_batch))
else:
elements.append(BoxColumn(definition.column_definition, box_batch))
element_index += 1

return grid_type(definition, elements)
if grid_type is BoxRow:
return BoxRow(definition.row_definition, elements)
else:
return BoxColumn(definition.column_definition, elements)


def create_box_layout(
definition: BoxLayoutDefinition, boxes: List[Box]
definition: BoxGridDefinition, boxes: List[Box]
) -> Union[BoxRow, BoxColumn]:
"""
Create a layout of boxes based on the given definition.
:param definition: BoxLayoutDefinition to use.
:param definition: BoxGridDefinition to use.
:param boxes: Boxes to include in the layout.
:return: BowRow or BoxColumn containing the given boxes.
"""
width_height = definition.num_columns, definition.num_rows
if width_height == (None, 1):
return BoxRow(definition, boxes)
return BoxRow(definition.row_definition, boxes)
elif width_height == (1, None):
return BoxColumn(definition, boxes)
return BoxColumn(definition.column_definition, boxes)
else:
return build_rectangular_grid(definition, boxes)
8 changes: 4 additions & 4 deletions shimmer/programmable/code_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from dataclasses import dataclass, field
from typing import List, cast

from shimmer.alignment import LeftBottom, PositionalAnchor
from shimmer.components.box_layout import BoxColumn, BoxLayoutDefinition
from shimmer.alignment import HorizontalAlignment
from shimmer.components.box_layout import BoxColumn, BoxColumnDefinition
from shimmer.programmable.instruction import (
InstructionDisplay,
InstructionDisplayDefinition,
Expand All @@ -13,15 +13,15 @@


@dataclass(frozen=True)
class CodeBlockDisplayDefinition(BoxLayoutDefinition):
class CodeBlockDisplayDefinition(BoxColumnDefinition):
"""Definition of how to display a CodeBlock."""

instruction_definition: InstructionDisplayDefinition = field(
default_factory=InstructionDisplayDefinition
)
# Spacing between the vertically arranged instructions.
spacing: int = 0
alignment: PositionalAnchor = LeftBottom
alignment: HorizontalAlignment = HorizontalAlignment.left


class CodeBlockDisplay(BoxColumn):
Expand Down
8 changes: 4 additions & 4 deletions shimmer/programmable/if_elif.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from dataclasses import dataclass, field
from typing import cast, Optional, List

from shimmer.alignment import LeftBottom, PositionalAnchor
from shimmer.alignment import HorizontalAlignment
from shimmer.components.box import Box
from shimmer.components.box_layout import BoxColumn, BoxLayoutDefinition
from shimmer.components.box_layout import BoxColumn, BoxColumnDefinition
from shimmer.programmable.code_block import (
CodeBlockDisplay,
CodeBlockDisplayDefinition,
Expand All @@ -21,7 +21,7 @@


@dataclass(frozen=True)
class InstructionWithBlockDisplayDefinition(BoxLayoutDefinition):
class InstructionWithBlockDisplayDefinition(BoxColumnDefinition):
"""Definition of how to display an instruction with an associated CodeBlock."""

# Definition of the main If statement display
Expand All @@ -35,7 +35,7 @@ class InstructionWithBlockDisplayDefinition(BoxLayoutDefinition):
# How far the code block is indented by compared to the If instruction.
code_block_indentation: int = 20
spacing: int = 0
alignment: PositionalAnchor = LeftBottom
alignment: HorizontalAlignment = HorizontalAlignment.left


class InstructionWithBlockDisplay(Box):
Expand Down
Loading

0 comments on commit d4437e1

Please sign in to comment.