pytermgui.window_manager.layouts

Layouts for the WindowManager.

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

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):
View Source
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

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):
View Source
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]

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):
View Source
 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})"

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:
View Source
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

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:
View Source
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

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:
View Source
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

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:
View Source
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.")

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')
View Source
181    def __init__(self, name: str = "Layout") -> None:
182        self.name = name
183        self.slots: list[Slot] = []

Returns the current global terminal instance.

#   def build_rows(self) -> list[list[pytermgui.window_manager.layouts.Slot]]:
View Source
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

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:
View Source
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

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:
View Source
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)

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:
View Source
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()

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:
View Source
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)

Applies the layout to each slot.