pytermgui.window_manager.layouts

Layouts for the WindowManager.

  1"""Layouts for the WindowManager."""
  2
  3from __future__ import annotations
  4
  5from dataclasses import dataclass
  6from typing import Callable
  7
  8from ..terminal import Terminal, get_terminal
  9from ..widgets import Widget
 10
 11
 12class Dimension:
 13    """The base class for layout dimensions.
 14
 15    Each dimension has a `value` property. This returns an integer,
 16    and is essentially the *meaning* of the object.
 17    """
 18
 19    _value: int
 20
 21    @property
 22    def value(self) -> int:
 23        """Returns the value of the object.
 24
 25        Override this for custom behaviour."""
 26
 27        return self._value
 28
 29    @value.setter
 30    def value(self, new: int) -> None:
 31        """Sets a new value."""
 32
 33        self._value = new
 34
 35    def __repr__(self) -> str:
 36        """Returns `{typename}(value={value})`.
 37
 38        We use this over the dataclasses one as that used `_value`, and it's
 39        a bit ugly.
 40        """
 41
 42        return f"{type(self).__name__}(value={self.value})"
 43
 44
 45@dataclass(repr=False, frozen=True)
 46class Static(Dimension):
 47    """A static dimension.
 48
 49    This dimension is immutable, and the Layout will always leave it unchanged.
 50    """
 51
 52    _value: int = 0
 53
 54
 55@dataclass(repr=False)
 56class Relative(Dimension):
 57    """A relative dimension.
 58
 59    This dimension has a scale attribute and bound method. Every time  the `value`
 60    is queried, `int(self.bound() * self.scale)` is returned.
 61
 62    When instantiated through `Layout.add_slot`, `bound` will default to either
 63    the terminal's width or height, depending on which attribute it is applied to.
 64    """
 65
 66    _value = 0
 67    scale: float
 68    bound: Callable[[], int]
 69
 70    @property
 71    def value(self) -> int:
 72        """Calculates the new value for the dimension."""
 73
 74        return int(self.bound() * self.scale)
 75
 76    @value.setter
 77    def value(self, new: int) -> None:
 78        """Disallows setting the value.
 79
 80        We can't inherit and then override a set-get property with a get one, so this
 81        kind of patches that issue up.
 82        """
 83
 84        raise TypeError
 85
 86    def __repr__(self) -> str:
 87        scale = self.scale
 88        bound = self.bound
 89
 90        original = super().__repr__()
 91        return original[:-1] + f", {scale=}, {bound=}" + original[-1]
 92
 93
 94@dataclass
 95class Auto(Dimension):
 96    """An automatically calculated dimension.
 97
 98    The value of this dimension is overwritten on `Layout.apply`.
 99
100    Generally, the way calculations are done is by looking at the available
101    size of the layout by subtracting the sum of all the non-auto dimensions
102    from the terminal's width or height, and dividing it by the number of
103    Auto-type dimensions in the current context.
104
105    An additional offset is applied to the first dimension (left-most or top-most)
106    of the context when the division has a remainder.
107    """
108
109    _value = 0
110
111    def __repr__(self) -> str:
112        return f"{type(self).__name__}(value={self.value})"
113
114
115@dataclass
116class Slot:
117    """A slot within a layout.
118
119    A slot has a name, width & height, as well as some content. It's `apply` method
120    can be called to apply the slot's position & dimensions to its content.
121    """
122
123    name: str
124    width: Dimension
125    height: Dimension
126
127    content: Widget | None = None
128
129    _restore_data: tuple[int, int, tuple[int, int]] | None = None
130
131    def apply(self, position: tuple[int, int]) -> None:
132        """Applies the given position & dimension to the content.
133
134        Args:
135            position: The position that this object resides in. Set as its content's `pos`.
136        """
137
138        if self.content is None or self.width is None or self.height is None:
139            return
140
141        if self._restore_data is None:
142            self._restore_data = (
143                self.content.width,
144                self.content.height,
145                self.content.pos,
146            )
147
148        self.content.height = self.height.value
149        self.content.width = self.width.value
150        self.content.pos = position
151
152    def detach_content(self) -> None:
153        """Detaches content & restores its original state."""
154
155        content = self.content
156        if content is None:
157            raise AttributeError(f"No content to detach in {self!r}.")
158
159        assert self._restore_data is not None
160
161        content.width, content.height, content.pos = self._restore_data
162
163        self.content = None
164        self._restore_data = None
165
166
167ROW_BREAK = Slot("Row Break", Static(0), Static(0))
168"""When encountered in `Layout.build_rows`, a new row will be started at the next element."""
169
170
171class Layout:
172    """Defines a layout of Widgets, used by WindowManager.
173
174    Internally, it keeps track of a list of `Slot`. This list is then turned into a list
175    of rows, all containing slots. This is done either when the current row has run out
176    of the terminal's width, or `ROW_BREAK` is encountered.
177    """
178
179    name: str
180
181    def __init__(self, name: str = "Layout") -> None:
182        self.name = name
183        self.slots: list[Slot] = []
184
185    @property
186    def terminal(self) -> Terminal:
187        """Returns the current global terminal instance."""
188
189        return get_terminal()
190
191    def _to_rows(self) -> list[list[Slot]]:
192        """Breaks `self.slots` into a list of list of slots.
193
194        The terminal's remaining width is kept track of, and when a slot doesn't have enough
195        space left it is pushed to a new row. Additionally, `ROW_BREAK` will force a new
196        row to be created, starting with the next slot.
197        """
198
199        rows: list[list[Slot]] = []
200        available = self.terminal.width
201
202        row: list[Slot] = []
203        for slot in self.slots:
204            if available <= 0 or slot is ROW_BREAK:
205                rows.append(row)
206
207                row = []
208                available = self.terminal.width - slot.width.value
209
210            if slot is ROW_BREAK:
211                continue
212
213            available -= slot.width.value
214            row.append(slot)
215
216        if len(row) > 0:
217            rows.append(row)
218
219        return rows
220
221    def build_rows(self) -> list[list[Slot]]:
222        """Builds a list of slot rows, breaking them & applying automatic dimensions.
223
224        Returns:
225            A list[list[Slot]], aka. a list of slot-rows.
226        """
227
228        def _get_height(row: list[Slot]) -> int:
229            defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))
230
231            if len(defined) > 0:
232                return max(slot.height.value for slot in defined)
233
234            return 0
235
236        def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
237            defined: list[Slot] = list(
238                filter(lambda slt: not isinstance(slt.width, Auto), row)
239            )
240            undefined = list(filter(lambda slt: slt not in defined, row))
241
242            available = self.terminal.width - sum(slot.width.value for slot in defined)
243
244            return divmod(available, len(undefined) or 1)
245
246        rows = self._to_rows()
247        heights = [_get_height(row) for row in rows]
248
249        occupied = sum(heights)
250        auto_height, extra_height = divmod(
251            self.terminal.height - occupied, heights.count(0) or 1
252        )
253
254        for row, height in zip(rows, heights):
255            height = height or auto_height
256
257            auto_width, extra_width = _calculate_widths(row)
258            for slot in row:
259                width = auto_width if isinstance(slot.width, Auto) else slot.width.value
260
261                if isinstance(slot.height, Auto):
262                    slot.height.value = height + extra_height
263                    extra_height = 0
264
265                if isinstance(slot.width, Auto):
266                    slot.width.value = width + extra_width
267                    extra_width = 0
268
269        return rows
270
271    def add_slot(
272        self,
273        name: str = "Slot",
274        *,
275        slot: Slot | None = None,
276        width: Dimension | int | float | None = None,
277        height: Dimension | int | float | None = None,
278        index: int = -1,
279    ) -> Slot:
280        """Adds a new slot to the layout.
281
282        Args:
283            name: The name of the slot. Used for display purposes.
284            slot: An already instantiated `Slot` instance. If this is given,
285                the additional width & height arguments will be ignored.
286            width: The width for the new slot. See below for special types.
287            height: The height for the new slot. See below for special types.
288            index: The index to add the new slot to.
289
290        Returns:
291            The just-added slot.
292
293        When defining dimensions, either width or height, some special value
294        types can be given:
295        - `Dimension`: Passed directly to the new slot.
296        - `None`: An `Auto` dimension is created with no value.
297        - `int`: A `Static` dimension is created with the given value.
298        - `float`: A `Relative` dimension is created with the given value as its
299            scale. Its `bound` attribute will default to the relevant part of the
300            terminal's size.
301        """
302
303        if slot is None:
304            if width is None:
305                width = Auto()
306
307            elif isinstance(width, int):
308                width = Static(width)
309
310            elif isinstance(width, float):
311                width = Relative(width, bound=lambda: self.terminal.width)
312
313            if height is None:
314                height = Auto()
315
316            elif isinstance(height, int):
317                height = Static(height)
318
319            elif isinstance(height, float):
320                height = Relative(height, bound=lambda: self.terminal.height)
321
322            slot = Slot(name, width=width, height=height)
323
324        if index == -1:
325            self.slots.append(slot)
326            return slot
327
328        self.slots.insert(index, slot)
329
330        return slot
331
332    def add_break(self, *, index: int = -1) -> None:
333        """Adds `ROW_BREAK` to the given index.
334
335        This special slot is ignored for all intents and purposes, other than when
336        breaking the slots into rows. In that context, when encountered, the current
337        row is deemed completed, and the next slot will go into a new row list.
338        """
339
340        self.add_slot(slot=ROW_BREAK, index=index)
341
342    def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
343        """Assigns a widget to the slot at the specified index.
344
345        Args:
346            widget: The widget to assign.
347            index: The target slot's index.
348            apply: If set, `apply` will be called once the widget has been assigned.
349        """
350
351        slots = [slot for slot in self.slots if slot is not ROW_BREAK]
352        if index > len(slots) - 1:
353            return
354
355        slot = slots[index]
356
357        slot.content = widget
358
359        if apply:
360            self.apply()
361
362    def apply(self) -> None:
363        """Applies the layout to each slot."""
364
365        position = list(self.terminal.origin)
366        for row in self.build_rows():
367            position[0] = 1
368
369            for slot in row:
370                slot.apply((position[0], position[1]))
371
372                position[0] += slot.width.value
373
374            position[1] += max(slot.height.value for slot in row)
375
376    def __getattr__(self, attr: str) -> Slot:
377        """Gets a slot by its (slugified) name."""
378
379        def _snakeify(name: str) -> str:
380            return name.lower().replace(" ", "_")
381
382        for slot in self.slots:
383            if _snakeify(slot.name) == attr:
384                return slot
385
386        raise AttributeError(f"Slot with name {attr!r} could not be found.")
class Dimension:
13class Dimension:
14    """The base class for layout dimensions.
15
16    Each dimension has a `value` property. This returns an integer,
17    and is essentially the *meaning* of the object.
18    """
19
20    _value: int
21
22    @property
23    def value(self) -> int:
24        """Returns the value of the object.
25
26        Override this for custom behaviour."""
27
28        return self._value
29
30    @value.setter
31    def value(self, new: int) -> None:
32        """Sets a new value."""
33
34        self._value = new
35
36    def __repr__(self) -> str:
37        """Returns `{typename}(value={value})`.
38
39        We use this over the dataclasses one as that used `_value`, and it's
40        a bit ugly.
41        """
42
43        return f"{type(self).__name__}(value={self.value})"

The base class for layout dimensions.

Each dimension has a value property. This returns an integer, and is essentially the meaning of the object.

Dimension()
value: int

Returns the value of the object.

Override this for custom behaviour.

@dataclass(repr=False, frozen=True)
class Static(Dimension):
46@dataclass(repr=False, frozen=True)
47class Static(Dimension):
48    """A static dimension.
49
50    This dimension is immutable, and the Layout will always leave it unchanged.
51    """
52
53    _value: int = 0

A static dimension.

This dimension is immutable, and the Layout will always leave it unchanged.

Static(_value: int = 0)
Inherited Members
Dimension
value
@dataclass(repr=False)
class Relative(Dimension):
56@dataclass(repr=False)
57class Relative(Dimension):
58    """A relative dimension.
59
60    This dimension has a scale attribute and bound method. Every time  the `value`
61    is queried, `int(self.bound() * self.scale)` is returned.
62
63    When instantiated through `Layout.add_slot`, `bound` will default to either
64    the terminal's width or height, depending on which attribute it is applied to.
65    """
66
67    _value = 0
68    scale: float
69    bound: Callable[[], int]
70
71    @property
72    def value(self) -> int:
73        """Calculates the new value for the dimension."""
74
75        return int(self.bound() * self.scale)
76
77    @value.setter
78    def value(self, new: int) -> None:
79        """Disallows setting the value.
80
81        We can't inherit and then override a set-get property with a get one, so this
82        kind of patches that issue up.
83        """
84
85        raise TypeError
86
87    def __repr__(self) -> str:
88        scale = self.scale
89        bound = self.bound
90
91        original = super().__repr__()
92        return original[:-1] + f", {scale=}, {bound=}" + original[-1]

A relative dimension.

This dimension has a scale attribute and bound method. Every time the value is queried, int(self.bound() * self.scale) is returned.

When instantiated through Layout.add_slot, bound will default to either the terminal's width or height, depending on which attribute it is applied to.

Relative(scale: float, bound: Callable[[], int])
value: int

Calculates the new value for the dimension.

@dataclass
class Auto(Dimension):
 95@dataclass
 96class Auto(Dimension):
 97    """An automatically calculated dimension.
 98
 99    The value of this dimension is overwritten on `Layout.apply`.
100
101    Generally, the way calculations are done is by looking at the available
102    size of the layout by subtracting the sum of all the non-auto dimensions
103    from the terminal's width or height, and dividing it by the number of
104    Auto-type dimensions in the current context.
105
106    An additional offset is applied to the first dimension (left-most or top-most)
107    of the context when the division has a remainder.
108    """
109
110    _value = 0
111
112    def __repr__(self) -> str:
113        return f"{type(self).__name__}(value={self.value})"

An automatically calculated dimension.

The value of this dimension is overwritten on Layout.apply.

Generally, the way calculations are done is by looking at the available size of the layout by subtracting the sum of all the non-auto dimensions from the terminal's width or height, and dividing it by the number of Auto-type dimensions in the current context.

An additional offset is applied to the first dimension (left-most or top-most) of the context when the division has a remainder.

Auto()
Inherited Members
Dimension
value
@dataclass
class Slot:
116@dataclass
117class Slot:
118    """A slot within a layout.
119
120    A slot has a name, width & height, as well as some content. It's `apply` method
121    can be called to apply the slot's position & dimensions to its content.
122    """
123
124    name: str
125    width: Dimension
126    height: Dimension
127
128    content: Widget | None = None
129
130    _restore_data: tuple[int, int, tuple[int, int]] | None = None
131
132    def apply(self, position: tuple[int, int]) -> None:
133        """Applies the given position & dimension to the content.
134
135        Args:
136            position: The position that this object resides in. Set as its content's `pos`.
137        """
138
139        if self.content is None or self.width is None or self.height is None:
140            return
141
142        if self._restore_data is None:
143            self._restore_data = (
144                self.content.width,
145                self.content.height,
146                self.content.pos,
147            )
148
149        self.content.height = self.height.value
150        self.content.width = self.width.value
151        self.content.pos = position
152
153    def detach_content(self) -> None:
154        """Detaches content & restores its original state."""
155
156        content = self.content
157        if content is None:
158            raise AttributeError(f"No content to detach in {self!r}.")
159
160        assert self._restore_data is not None
161
162        content.width, content.height, content.pos = self._restore_data
163
164        self.content = None
165        self._restore_data = None

A slot within a layout.

A slot has a name, width & height, as well as some content. It's apply method can be called to apply the slot's position & dimensions to its content.

Slot( name: str, width: pytermgui.window_manager.layouts.Dimension, height: pytermgui.window_manager.layouts.Dimension, content: pytermgui.widgets.base.Widget | None = None, _restore_data: tuple[int, int, tuple[int, int]] | None = None)
content: pytermgui.widgets.base.Widget | None = None
def apply(self, position: tuple[int, int]) -> None:
132    def apply(self, position: tuple[int, int]) -> None:
133        """Applies the given position & dimension to the content.
134
135        Args:
136            position: The position that this object resides in. Set as its content's `pos`.
137        """
138
139        if self.content is None or self.width is None or self.height is None:
140            return
141
142        if self._restore_data is None:
143            self._restore_data = (
144                self.content.width,
145                self.content.height,
146                self.content.pos,
147            )
148
149        self.content.height = self.height.value
150        self.content.width = self.width.value
151        self.content.pos = position

Applies the given position & dimension to the content.

Args
  • position: The position that this object resides in. Set as its content's pos.
def detach_content(self) -> None:
153    def detach_content(self) -> None:
154        """Detaches content & restores its original state."""
155
156        content = self.content
157        if content is None:
158            raise AttributeError(f"No content to detach in {self!r}.")
159
160        assert self._restore_data is not None
161
162        content.width, content.height, content.pos = self._restore_data
163
164        self.content = None
165        self._restore_data = None

Detaches content & restores its original state.

ROW_BREAK = Slot(name='Row Break', width=Static(value=0), height=Static(value=0), content=None, _restore_data=None)

When encountered in Layout.build_rows, a new row will be started at the next element.

class Layout:
172class Layout:
173    """Defines a layout of Widgets, used by WindowManager.
174
175    Internally, it keeps track of a list of `Slot`. This list is then turned into a list
176    of rows, all containing slots. This is done either when the current row has run out
177    of the terminal's width, or `ROW_BREAK` is encountered.
178    """
179
180    name: str
181
182    def __init__(self, name: str = "Layout") -> None:
183        self.name = name
184        self.slots: list[Slot] = []
185
186    @property
187    def terminal(self) -> Terminal:
188        """Returns the current global terminal instance."""
189
190        return get_terminal()
191
192    def _to_rows(self) -> list[list[Slot]]:
193        """Breaks `self.slots` into a list of list of slots.
194
195        The terminal's remaining width is kept track of, and when a slot doesn't have enough
196        space left it is pushed to a new row. Additionally, `ROW_BREAK` will force a new
197        row to be created, starting with the next slot.
198        """
199
200        rows: list[list[Slot]] = []
201        available = self.terminal.width
202
203        row: list[Slot] = []
204        for slot in self.slots:
205            if available <= 0 or slot is ROW_BREAK:
206                rows.append(row)
207
208                row = []
209                available = self.terminal.width - slot.width.value
210
211            if slot is ROW_BREAK:
212                continue
213
214            available -= slot.width.value
215            row.append(slot)
216
217        if len(row) > 0:
218            rows.append(row)
219
220        return rows
221
222    def build_rows(self) -> list[list[Slot]]:
223        """Builds a list of slot rows, breaking them & applying automatic dimensions.
224
225        Returns:
226            A list[list[Slot]], aka. a list of slot-rows.
227        """
228
229        def _get_height(row: list[Slot]) -> int:
230            defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))
231
232            if len(defined) > 0:
233                return max(slot.height.value for slot in defined)
234
235            return 0
236
237        def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
238            defined: list[Slot] = list(
239                filter(lambda slt: not isinstance(slt.width, Auto), row)
240            )
241            undefined = list(filter(lambda slt: slt not in defined, row))
242
243            available = self.terminal.width - sum(slot.width.value for slot in defined)
244
245            return divmod(available, len(undefined) or 1)
246
247        rows = self._to_rows()
248        heights = [_get_height(row) for row in rows]
249
250        occupied = sum(heights)
251        auto_height, extra_height = divmod(
252            self.terminal.height - occupied, heights.count(0) or 1
253        )
254
255        for row, height in zip(rows, heights):
256            height = height or auto_height
257
258            auto_width, extra_width = _calculate_widths(row)
259            for slot in row:
260                width = auto_width if isinstance(slot.width, Auto) else slot.width.value
261
262                if isinstance(slot.height, Auto):
263                    slot.height.value = height + extra_height
264                    extra_height = 0
265
266                if isinstance(slot.width, Auto):
267                    slot.width.value = width + extra_width
268                    extra_width = 0
269
270        return rows
271
272    def add_slot(
273        self,
274        name: str = "Slot",
275        *,
276        slot: Slot | None = None,
277        width: Dimension | int | float | None = None,
278        height: Dimension | int | float | None = None,
279        index: int = -1,
280    ) -> Slot:
281        """Adds a new slot to the layout.
282
283        Args:
284            name: The name of the slot. Used for display purposes.
285            slot: An already instantiated `Slot` instance. If this is given,
286                the additional width & height arguments will be ignored.
287            width: The width for the new slot. See below for special types.
288            height: The height for the new slot. See below for special types.
289            index: The index to add the new slot to.
290
291        Returns:
292            The just-added slot.
293
294        When defining dimensions, either width or height, some special value
295        types can be given:
296        - `Dimension`: Passed directly to the new slot.
297        - `None`: An `Auto` dimension is created with no value.
298        - `int`: A `Static` dimension is created with the given value.
299        - `float`: A `Relative` dimension is created with the given value as its
300            scale. Its `bound` attribute will default to the relevant part of the
301            terminal's size.
302        """
303
304        if slot is None:
305            if width is None:
306                width = Auto()
307
308            elif isinstance(width, int):
309                width = Static(width)
310
311            elif isinstance(width, float):
312                width = Relative(width, bound=lambda: self.terminal.width)
313
314            if height is None:
315                height = Auto()
316
317            elif isinstance(height, int):
318                height = Static(height)
319
320            elif isinstance(height, float):
321                height = Relative(height, bound=lambda: self.terminal.height)
322
323            slot = Slot(name, width=width, height=height)
324
325        if index == -1:
326            self.slots.append(slot)
327            return slot
328
329        self.slots.insert(index, slot)
330
331        return slot
332
333    def add_break(self, *, index: int = -1) -> None:
334        """Adds `ROW_BREAK` to the given index.
335
336        This special slot is ignored for all intents and purposes, other than when
337        breaking the slots into rows. In that context, when encountered, the current
338        row is deemed completed, and the next slot will go into a new row list.
339        """
340
341        self.add_slot(slot=ROW_BREAK, index=index)
342
343    def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
344        """Assigns a widget to the slot at the specified index.
345
346        Args:
347            widget: The widget to assign.
348            index: The target slot's index.
349            apply: If set, `apply` will be called once the widget has been assigned.
350        """
351
352        slots = [slot for slot in self.slots if slot is not ROW_BREAK]
353        if index > len(slots) - 1:
354            return
355
356        slot = slots[index]
357
358        slot.content = widget
359
360        if apply:
361            self.apply()
362
363    def apply(self) -> None:
364        """Applies the layout to each slot."""
365
366        position = list(self.terminal.origin)
367        for row in self.build_rows():
368            position[0] = 1
369
370            for slot in row:
371                slot.apply((position[0], position[1]))
372
373                position[0] += slot.width.value
374
375            position[1] += max(slot.height.value for slot in row)
376
377    def __getattr__(self, attr: str) -> Slot:
378        """Gets a slot by its (slugified) name."""
379
380        def _snakeify(name: str) -> str:
381            return name.lower().replace(" ", "_")
382
383        for slot in self.slots:
384            if _snakeify(slot.name) == attr:
385                return slot
386
387        raise AttributeError(f"Slot with name {attr!r} could not be found.")

Defines a layout of Widgets, used by WindowManager.

Internally, it keeps track of a list of Slot. This list is then turned into a list of rows, all containing slots. This is done either when the current row has run out of the terminal's width, or ROW_BREAK is encountered.

Layout(name: str = 'Layout')
182    def __init__(self, name: str = "Layout") -> None:
183        self.name = name
184        self.slots: list[Slot] = []

Returns the current global terminal instance.

def build_rows(self) -> list[list[pytermgui.window_manager.layouts.Slot]]:
222    def build_rows(self) -> list[list[Slot]]:
223        """Builds a list of slot rows, breaking them & applying automatic dimensions.
224
225        Returns:
226            A list[list[Slot]], aka. a list of slot-rows.
227        """
228
229        def _get_height(row: list[Slot]) -> int:
230            defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))
231
232            if len(defined) > 0:
233                return max(slot.height.value for slot in defined)
234
235            return 0
236
237        def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
238            defined: list[Slot] = list(
239                filter(lambda slt: not isinstance(slt.width, Auto), row)
240            )
241            undefined = list(filter(lambda slt: slt not in defined, row))
242
243            available = self.terminal.width - sum(slot.width.value for slot in defined)
244
245            return divmod(available, len(undefined) or 1)
246
247        rows = self._to_rows()
248        heights = [_get_height(row) for row in rows]
249
250        occupied = sum(heights)
251        auto_height, extra_height = divmod(
252            self.terminal.height - occupied, heights.count(0) or 1
253        )
254
255        for row, height in zip(rows, heights):
256            height = height or auto_height
257
258            auto_width, extra_width = _calculate_widths(row)
259            for slot in row:
260                width = auto_width if isinstance(slot.width, Auto) else slot.width.value
261
262                if isinstance(slot.height, Auto):
263                    slot.height.value = height + extra_height
264                    extra_height = 0
265
266                if isinstance(slot.width, Auto):
267                    slot.width.value = width + extra_width
268                    extra_width = 0
269
270        return rows

Builds a list of slot rows, breaking them & applying automatic dimensions.

Returns

A list[list[Slot]], aka. a list of slot-rows.

def add_slot( self, name: str = 'Slot', *, slot: pytermgui.window_manager.layouts.Slot | None = None, width: pytermgui.window_manager.layouts.Dimension | int | float | None = None, height: pytermgui.window_manager.layouts.Dimension | int | float | None = None, index: int = -1) -> pytermgui.window_manager.layouts.Slot:
272    def add_slot(
273        self,
274        name: str = "Slot",
275        *,
276        slot: Slot | None = None,
277        width: Dimension | int | float | None = None,
278        height: Dimension | int | float | None = None,
279        index: int = -1,
280    ) -> Slot:
281        """Adds a new slot to the layout.
282
283        Args:
284            name: The name of the slot. Used for display purposes.
285            slot: An already instantiated `Slot` instance. If this is given,
286                the additional width & height arguments will be ignored.
287            width: The width for the new slot. See below for special types.
288            height: The height for the new slot. See below for special types.
289            index: The index to add the new slot to.
290
291        Returns:
292            The just-added slot.
293
294        When defining dimensions, either width or height, some special value
295        types can be given:
296        - `Dimension`: Passed directly to the new slot.
297        - `None`: An `Auto` dimension is created with no value.
298        - `int`: A `Static` dimension is created with the given value.
299        - `float`: A `Relative` dimension is created with the given value as its
300            scale. Its `bound` attribute will default to the relevant part of the
301            terminal's size.
302        """
303
304        if slot is None:
305            if width is None:
306                width = Auto()
307
308            elif isinstance(width, int):
309                width = Static(width)
310
311            elif isinstance(width, float):
312                width = Relative(width, bound=lambda: self.terminal.width)
313
314            if height is None:
315                height = Auto()
316
317            elif isinstance(height, int):
318                height = Static(height)
319
320            elif isinstance(height, float):
321                height = Relative(height, bound=lambda: self.terminal.height)
322
323            slot = Slot(name, width=width, height=height)
324
325        if index == -1:
326            self.slots.append(slot)
327            return slot
328
329        self.slots.insert(index, slot)
330
331        return slot

Adds a new slot to the layout.

Args
  • name: The name of the slot. Used for display purposes.
  • slot: An already instantiated Slot instance. If this is given, the additional width & height arguments will be ignored.
  • width: The width for the new slot. See below for special types.
  • height: The height for the new slot. See below for special types.
  • index: The index to add the new slot to.
Returns

The just-added slot.

When defining dimensions, either width or height, some special value types can be given:

  • Dimension: Passed directly to the new slot.
  • None: An Auto dimension is created with no value.
  • int: A Static dimension is created with the given value.
  • float: A Relative dimension is created with the given value as its scale. Its bound attribute will default to the relevant part of the terminal's size.
def add_break(self, *, index: int = -1) -> None:
333    def add_break(self, *, index: int = -1) -> None:
334        """Adds `ROW_BREAK` to the given index.
335
336        This special slot is ignored for all intents and purposes, other than when
337        breaking the slots into rows. In that context, when encountered, the current
338        row is deemed completed, and the next slot will go into a new row list.
339        """
340
341        self.add_slot(slot=ROW_BREAK, index=index)

Adds ROW_BREAK to the given index.

This special slot is ignored for all intents and purposes, other than when breaking the slots into rows. In that context, when encountered, the current row is deemed completed, and the next slot will go into a new row list.

def assign( self, widget: pytermgui.widgets.base.Widget, *, index: int = -1, apply: bool = True) -> None:
343    def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
344        """Assigns a widget to the slot at the specified index.
345
346        Args:
347            widget: The widget to assign.
348            index: The target slot's index.
349            apply: If set, `apply` will be called once the widget has been assigned.
350        """
351
352        slots = [slot for slot in self.slots if slot is not ROW_BREAK]
353        if index > len(slots) - 1:
354            return
355
356        slot = slots[index]
357
358        slot.content = widget
359
360        if apply:
361            self.apply()

Assigns a widget to the slot at the specified index.

Args
  • widget: The widget to assign.
  • index: The target slot's index.
  • apply: If set, apply will be called once the widget has been assigned.
def apply(self) -> None:
363    def apply(self) -> None:
364        """Applies the layout to each slot."""
365
366        position = list(self.terminal.origin)
367        for row in self.build_rows():
368            position[0] = 1
369
370            for slot in row:
371                slot.apply((position[0], position[1]))
372
373                position[0] += slot.width.value
374
375            position[1] += max(slot.height.value for slot in row)

Applies the layout to each slot.