classes.c22_frame_iterator

  1from collections.abc import Callable
  2from classes import KBox
  3from lib import layout
  4from icecream import ic
  5from typing import Any, Literal
  6
  7PageSetupFunc = Callable[[int], layout.Coordinates]
  8"""A callable type for functions that set up a new page.
  9
 10The function takes one argument:
 11- `pageIndex`: The index of the page being created.
 12
 13Returns a tuple representing the (x, y, width, height) of the content frame.
 14"""
 15
 16VerticalDirection = Literal["top-down", "bottom-up"]
 17
 18
 19ItemDrawFunc = Callable[[KBox, Any, int, int], int | None]
 20"""A callable type for functions used in KFrameIterator.
 21
 22The function takes three arguments:
 23- `box`: An instance of `KBox` representing the current box.
 24- `item`: The current item from the items list.
 25- `itemIndex`: The index of the current item.
 26- `rowIndex`: The index of the current row.
 27
 28Returns the height used to draw the item or 0 if there was insufficient space and a new page is needed. If the function returns None, it indicates that the item was skipped.
 29"""
 30
 31
 32class KFrameIterator:
 33    PageSetup = PageSetupFunc
 34    ItemDraw = ItemDrawFunc
 35
 36    def __init__(
 37        self,
 38        pageSetup: PageSetupFunc,
 39        itemDraw: ItemDrawFunc,
 40        items: list[Any],
 41        pageBreakBefore: Callable[[Any], bool] | None = None,
 42        pageBreakBeforeKey: str | None = None,
 43        verticalDirection: VerticalDirection = "top-down",
 44    ):
 45        """
 46        Initialize a KFrameIterator instance.
 47
 48        Args:
 49            pageSetup: A function to set up a new page and return the content frame.
 50            itemDraw: A function to call for each item.
 51            items: A list of items to iterate over.
 52            pageBreakBefore: Optional predicate to request a page break before drawing an item.
 53            pageBreakBeforeKey: Optional key checked on mapping-like items for page-break request.
 54            verticalDirection: Vertical flow direction. Defaults to "top-down".
 55        """
 56        self.pageSetup = pageSetup
 57        self.itemDraw = itemDraw
 58        self.items = items
 59        self.pageBreakBefore = pageBreakBefore
 60        self.pageBreakBeforeKey = pageBreakBeforeKey
 61        if verticalDirection not in ("top-down", "bottom-up"):
 62            raise ValueError(
 63                f"Invalid verticalDirection '{verticalDirection}'. Use 'top-down' or 'bottom-up'."
 64            )
 65        self.verticalDirection = verticalDirection
 66        self.pageIndex = 0
 67        self.itemIndex = 0
 68        self.rowIndex = 0
 69        self.history: list[dict] = []
 70
 71        # Initialize the first page and set the content frame for the first time
 72        self.box = self.createPage()
 73
 74    def shouldPageBreakBefore(self, item: Any) -> bool:
 75        """Return True if a page break is requested before drawing the given item."""
 76        if self.pageBreakBefore is not None:
 77            return bool(self.pageBreakBefore(item))
 78
 79        if self.pageBreakBeforeKey and hasattr(item, "get"):
 80            return bool(item.get(self.pageBreakBeforeKey, False))
 81
 82        return False
 83
 84    def createPage(self) -> "KBox":
 85        """Add a new page and update the content frame."""
 86        box = KBox(self.pageSetup(self.pageIndex))
 87        self.pageIndex += 1
 88        self.rowIndex = 0
 89        return box
 90
 91    def getAdvanceOrigin(self) -> Literal["top", "bottom"]:
 92        """Get the side from which the frame is consumed after a successful draw."""
 93        return "bottom" if self.verticalDirection == "top-down" else "top"
 94
 95    def __iter__(self):
 96        return self
 97
 98    def __next__(self):
 99        if not self.items:
100            raise StopIteration
101
102        # Peek at the next item without removing it
103        item = self.items[0]
104
105        # Optional explicit page break before this item (ignored at top of page)
106        if self.rowIndex > 0 and self.shouldPageBreakBefore(item):
107            self.box = self.createPage()
108
109        # Try to draw the item
110        result = self.itemDraw(self.box, item, self.itemIndex, self.rowIndex)
111
112        if result == 0:
113            # Not enough space - create a new page
114            self.box = self.createPage()
115            # Try again on the new page
116            result = self.itemDraw(self.box, item, self.itemIndex, self.rowIndex)
117
118            if result == 0:
119                # Still not enough space - item too large for the frame
120                raise ValueError(
121                    f"Item at index {self.itemIndex} is too large for the frame."
122                )
123
124        # Item was drawn successfully, remove the item
125        self.items.pop(0)
126
127        # Advance the box and indices if the item was drawn
128        if result is not None:
129            self.history.append(
130                {
131                    "item": item,
132                    "itemIndex": self.itemIndex,
133                    "rowIndex": self.rowIndex,
134                    "pageIndex": self.pageIndex - 1,
135                }
136            )
137            self.box.shrinkHeight(
138                result, origin=self.getAdvanceOrigin(), implicitUnit="pt"
139            )
140            self.itemIndex += 1
141            self.rowIndex += 1
142
143    def iterate(self) -> "KFrameIterator":
144        """Iterate through all items, calling the function for each."""
145        for _ in self:
146            pass  # The function is already called in __next__
147        return self
PageSetupFunc = collections.abc.Callable[[int], tuple[float, float, float, float]]

A callable type for functions that set up a new page.

The function takes one argument:

  • pageIndex: The index of the page being created.

Returns a tuple representing the (x, y, width, height) of the content frame.

VerticalDirection = typing.Literal['top-down', 'bottom-up']
ItemDrawFunc = collections.abc.Callable[[classes.c20_box.KBox, typing.Any, int, int], int | None]

A callable type for functions used in KFrameIterator.

The function takes three arguments:

  • box: An instance of KBox representing the current box.
  • item: The current item from the items list.
  • itemIndex: The index of the current item.
  • rowIndex: The index of the current row.

Returns the height used to draw the item or 0 if there was insufficient space and a new page is needed. If the function returns None, it indicates that the item was skipped.

class KFrameIterator:
 33class KFrameIterator:
 34    PageSetup = PageSetupFunc
 35    ItemDraw = ItemDrawFunc
 36
 37    def __init__(
 38        self,
 39        pageSetup: PageSetupFunc,
 40        itemDraw: ItemDrawFunc,
 41        items: list[Any],
 42        pageBreakBefore: Callable[[Any], bool] | None = None,
 43        pageBreakBeforeKey: str | None = None,
 44        verticalDirection: VerticalDirection = "top-down",
 45    ):
 46        """
 47        Initialize a KFrameIterator instance.
 48
 49        Args:
 50            pageSetup: A function to set up a new page and return the content frame.
 51            itemDraw: A function to call for each item.
 52            items: A list of items to iterate over.
 53            pageBreakBefore: Optional predicate to request a page break before drawing an item.
 54            pageBreakBeforeKey: Optional key checked on mapping-like items for page-break request.
 55            verticalDirection: Vertical flow direction. Defaults to "top-down".
 56        """
 57        self.pageSetup = pageSetup
 58        self.itemDraw = itemDraw
 59        self.items = items
 60        self.pageBreakBefore = pageBreakBefore
 61        self.pageBreakBeforeKey = pageBreakBeforeKey
 62        if verticalDirection not in ("top-down", "bottom-up"):
 63            raise ValueError(
 64                f"Invalid verticalDirection '{verticalDirection}'. Use 'top-down' or 'bottom-up'."
 65            )
 66        self.verticalDirection = verticalDirection
 67        self.pageIndex = 0
 68        self.itemIndex = 0
 69        self.rowIndex = 0
 70        self.history: list[dict] = []
 71
 72        # Initialize the first page and set the content frame for the first time
 73        self.box = self.createPage()
 74
 75    def shouldPageBreakBefore(self, item: Any) -> bool:
 76        """Return True if a page break is requested before drawing the given item."""
 77        if self.pageBreakBefore is not None:
 78            return bool(self.pageBreakBefore(item))
 79
 80        if self.pageBreakBeforeKey and hasattr(item, "get"):
 81            return bool(item.get(self.pageBreakBeforeKey, False))
 82
 83        return False
 84
 85    def createPage(self) -> "KBox":
 86        """Add a new page and update the content frame."""
 87        box = KBox(self.pageSetup(self.pageIndex))
 88        self.pageIndex += 1
 89        self.rowIndex = 0
 90        return box
 91
 92    def getAdvanceOrigin(self) -> Literal["top", "bottom"]:
 93        """Get the side from which the frame is consumed after a successful draw."""
 94        return "bottom" if self.verticalDirection == "top-down" else "top"
 95
 96    def __iter__(self):
 97        return self
 98
 99    def __next__(self):
100        if not self.items:
101            raise StopIteration
102
103        # Peek at the next item without removing it
104        item = self.items[0]
105
106        # Optional explicit page break before this item (ignored at top of page)
107        if self.rowIndex > 0 and self.shouldPageBreakBefore(item):
108            self.box = self.createPage()
109
110        # Try to draw the item
111        result = self.itemDraw(self.box, item, self.itemIndex, self.rowIndex)
112
113        if result == 0:
114            # Not enough space - create a new page
115            self.box = self.createPage()
116            # Try again on the new page
117            result = self.itemDraw(self.box, item, self.itemIndex, self.rowIndex)
118
119            if result == 0:
120                # Still not enough space - item too large for the frame
121                raise ValueError(
122                    f"Item at index {self.itemIndex} is too large for the frame."
123                )
124
125        # Item was drawn successfully, remove the item
126        self.items.pop(0)
127
128        # Advance the box and indices if the item was drawn
129        if result is not None:
130            self.history.append(
131                {
132                    "item": item,
133                    "itemIndex": self.itemIndex,
134                    "rowIndex": self.rowIndex,
135                    "pageIndex": self.pageIndex - 1,
136                }
137            )
138            self.box.shrinkHeight(
139                result, origin=self.getAdvanceOrigin(), implicitUnit="pt"
140            )
141            self.itemIndex += 1
142            self.rowIndex += 1
143
144    def iterate(self) -> "KFrameIterator":
145        """Iterate through all items, calling the function for each."""
146        for _ in self:
147            pass  # The function is already called in __next__
148        return self
KFrameIterator( pageSetup: Callable[[int], tuple[float, float, float, float]], itemDraw: Callable[[classes.c20_box.KBox, typing.Any, int, int], int | None], items: list[typing.Any], pageBreakBefore: Callable[[typing.Any], bool] | None = None, pageBreakBeforeKey: str | None = None, verticalDirection: Literal['top-down', 'bottom-up'] = 'top-down')
37    def __init__(
38        self,
39        pageSetup: PageSetupFunc,
40        itemDraw: ItemDrawFunc,
41        items: list[Any],
42        pageBreakBefore: Callable[[Any], bool] | None = None,
43        pageBreakBeforeKey: str | None = None,
44        verticalDirection: VerticalDirection = "top-down",
45    ):
46        """
47        Initialize a KFrameIterator instance.
48
49        Args:
50            pageSetup: A function to set up a new page and return the content frame.
51            itemDraw: A function to call for each item.
52            items: A list of items to iterate over.
53            pageBreakBefore: Optional predicate to request a page break before drawing an item.
54            pageBreakBeforeKey: Optional key checked on mapping-like items for page-break request.
55            verticalDirection: Vertical flow direction. Defaults to "top-down".
56        """
57        self.pageSetup = pageSetup
58        self.itemDraw = itemDraw
59        self.items = items
60        self.pageBreakBefore = pageBreakBefore
61        self.pageBreakBeforeKey = pageBreakBeforeKey
62        if verticalDirection not in ("top-down", "bottom-up"):
63            raise ValueError(
64                f"Invalid verticalDirection '{verticalDirection}'. Use 'top-down' or 'bottom-up'."
65            )
66        self.verticalDirection = verticalDirection
67        self.pageIndex = 0
68        self.itemIndex = 0
69        self.rowIndex = 0
70        self.history: list[dict] = []
71
72        # Initialize the first page and set the content frame for the first time
73        self.box = self.createPage()

Initialize a KFrameIterator instance.

Arguments:
  • pageSetup: A function to set up a new page and return the content frame.
  • itemDraw: A function to call for each item.
  • items: A list of items to iterate over.
  • pageBreakBefore: Optional predicate to request a page break before drawing an item.
  • pageBreakBeforeKey: Optional key checked on mapping-like items for page-break request.
  • verticalDirection: Vertical flow direction. Defaults to "top-down".
PageSetup = collections.abc.Callable[[int], tuple[float, float, float, float]]
ItemDraw = collections.abc.Callable[[classes.c20_box.KBox, typing.Any, int, int], int | None]
pageSetup
itemDraw
items
pageBreakBefore
pageBreakBeforeKey
verticalDirection
pageIndex
itemIndex
rowIndex
history: list[dict]
box
def shouldPageBreakBefore(self, item: Any) -> bool:
75    def shouldPageBreakBefore(self, item: Any) -> bool:
76        """Return True if a page break is requested before drawing the given item."""
77        if self.pageBreakBefore is not None:
78            return bool(self.pageBreakBefore(item))
79
80        if self.pageBreakBeforeKey and hasattr(item, "get"):
81            return bool(item.get(self.pageBreakBeforeKey, False))
82
83        return False

Return True if a page break is requested before drawing the given item.

def createPage(self) -> classes.c20_box.KBox:
85    def createPage(self) -> "KBox":
86        """Add a new page and update the content frame."""
87        box = KBox(self.pageSetup(self.pageIndex))
88        self.pageIndex += 1
89        self.rowIndex = 0
90        return box

Add a new page and update the content frame.

def getAdvanceOrigin(self) -> Literal['top', 'bottom']:
92    def getAdvanceOrigin(self) -> Literal["top", "bottom"]:
93        """Get the side from which the frame is consumed after a successful draw."""
94        return "bottom" if self.verticalDirection == "top-down" else "top"

Get the side from which the frame is consumed after a successful draw.

def iterate(self) -> KFrameIterator:
144    def iterate(self) -> "KFrameIterator":
145        """Iterate through all items, calling the function for each."""
146        for _ in self:
147            pass  # The function is already called in __next__
148        return self

Iterate through all items, calling the function for each.