|
""" |
|
Layout dimensions are used to give the minimum, maximum and preferred |
|
dimensions for containers and controls. |
|
""" |
|
from __future__ import annotations |
|
|
|
from typing import TYPE_CHECKING, Any, Callable, Union |
|
|
|
__all__ = [ |
|
"Dimension", |
|
"D", |
|
"sum_layout_dimensions", |
|
"max_layout_dimensions", |
|
"AnyDimension", |
|
"to_dimension", |
|
"is_dimension", |
|
] |
|
|
|
if TYPE_CHECKING: |
|
from typing_extensions import TypeGuard |
|
|
|
|
|
class Dimension: |
|
""" |
|
Specified dimension (width/height) of a user control or window. |
|
|
|
The layout engine tries to honor the preferred size. If that is not |
|
possible, because the terminal is larger or smaller, it tries to keep in |
|
between min and max. |
|
|
|
:param min: Minimum size. |
|
:param max: Maximum size. |
|
:param weight: For a VSplit/HSplit, the actual size will be determined |
|
by taking the proportion of weights from all the children. |
|
E.g. When there are two children, one with a weight of 1, |
|
and the other with a weight of 2, the second will always be |
|
twice as big as the first, if the min/max values allow it. |
|
:param preferred: Preferred size. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
min: int | None = None, |
|
max: int | None = None, |
|
weight: int | None = None, |
|
preferred: int | None = None, |
|
) -> None: |
|
if weight is not None: |
|
assert weight >= 0 |
|
|
|
assert min is None or min >= 0 |
|
assert max is None or max >= 0 |
|
assert preferred is None or preferred >= 0 |
|
|
|
self.min_specified = min is not None |
|
self.max_specified = max is not None |
|
self.preferred_specified = preferred is not None |
|
self.weight_specified = weight is not None |
|
|
|
if min is None: |
|
min = 0 |
|
if max is None: |
|
max = 1000**10 |
|
if preferred is None: |
|
preferred = min |
|
if weight is None: |
|
weight = 1 |
|
|
|
self.min = min |
|
self.max = max |
|
self.preferred = preferred |
|
self.weight = weight |
|
|
|
|
|
if max < min: |
|
raise ValueError("Invalid Dimension: max < min.") |
|
|
|
|
|
if self.preferred < self.min: |
|
self.preferred = self.min |
|
|
|
if self.preferred > self.max: |
|
self.preferred = self.max |
|
|
|
@classmethod |
|
def exact(cls, amount: int) -> Dimension: |
|
""" |
|
Return a :class:`.Dimension` with an exact size. (min, max and |
|
preferred set to ``amount``). |
|
""" |
|
return cls(min=amount, max=amount, preferred=amount) |
|
|
|
@classmethod |
|
def zero(cls) -> Dimension: |
|
""" |
|
Create a dimension that represents a zero size. (Used for 'invisible' |
|
controls.) |
|
""" |
|
return cls.exact(amount=0) |
|
|
|
def is_zero(self) -> bool: |
|
"True if this `Dimension` represents a zero size." |
|
return self.preferred == 0 or self.max == 0 |
|
|
|
def __repr__(self) -> str: |
|
fields = [] |
|
if self.min_specified: |
|
fields.append("min=%r" % self.min) |
|
if self.max_specified: |
|
fields.append("max=%r" % self.max) |
|
if self.preferred_specified: |
|
fields.append("preferred=%r" % self.preferred) |
|
if self.weight_specified: |
|
fields.append("weight=%r" % self.weight) |
|
|
|
return "Dimension(%s)" % ", ".join(fields) |
|
|
|
|
|
def sum_layout_dimensions(dimensions: list[Dimension]) -> Dimension: |
|
""" |
|
Sum a list of :class:`.Dimension` instances. |
|
""" |
|
min = sum(d.min for d in dimensions) |
|
max = sum(d.max for d in dimensions) |
|
preferred = sum(d.preferred for d in dimensions) |
|
|
|
return Dimension(min=min, max=max, preferred=preferred) |
|
|
|
|
|
def max_layout_dimensions(dimensions: list[Dimension]) -> Dimension: |
|
""" |
|
Take the maximum of a list of :class:`.Dimension` instances. |
|
Used when we have a HSplit/VSplit, and we want to get the best width/height.) |
|
""" |
|
if not len(dimensions): |
|
return Dimension.zero() |
|
|
|
|
|
|
|
|
|
if all(d.is_zero() for d in dimensions): |
|
return dimensions[0] |
|
|
|
|
|
dimensions = [d for d in dimensions if not d.is_zero()] |
|
|
|
if dimensions: |
|
|
|
min_ = max(d.min for d in dimensions) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
max_ = min(d.max for d in dimensions) |
|
max_ = max(max_, max(d.preferred for d in dimensions)) |
|
|
|
|
|
|
|
|
|
|
|
if min_ > max_: |
|
max_ = min_ |
|
|
|
preferred = max(d.preferred for d in dimensions) |
|
|
|
return Dimension(min=min_, max=max_, preferred=preferred) |
|
else: |
|
return Dimension() |
|
|
|
|
|
|
|
AnyDimension = Union[ |
|
None, |
|
int, |
|
Dimension, |
|
|
|
Callable[[], Any], |
|
] |
|
|
|
|
|
def to_dimension(value: AnyDimension) -> Dimension: |
|
""" |
|
Turn the given object into a `Dimension` object. |
|
""" |
|
if value is None: |
|
return Dimension() |
|
if isinstance(value, int): |
|
return Dimension.exact(value) |
|
if isinstance(value, Dimension): |
|
return value |
|
if callable(value): |
|
return to_dimension(value()) |
|
|
|
raise ValueError("Not an integer or Dimension object.") |
|
|
|
|
|
def is_dimension(value: object) -> TypeGuard[AnyDimension]: |
|
""" |
|
Test whether the given value could be a valid dimension. |
|
(For usage in an assertion. It's not guaranteed in case of a callable.) |
|
""" |
|
if value is None: |
|
return True |
|
if callable(value): |
|
return True |
|
if isinstance(value, (int, Dimension)): |
|
return True |
|
return False |
|
|
|
|
|
|
|
D = Dimension |
|
|
|
|
|
LayoutDimension = Dimension |
|
|