1"""
2Layout dimensions are used to give the minimum, maximum and preferred
3dimensions for containers and controls.
4"""
5
6from __future__ import annotations
7
8from typing import TYPE_CHECKING, Any, Callable, Union
9
10__all__ = [
11 "Dimension",
12 "D",
13 "sum_layout_dimensions",
14 "max_layout_dimensions",
15 "AnyDimension",
16 "to_dimension",
17 "is_dimension",
18]
19
20if TYPE_CHECKING:
21 from typing_extensions import TypeGuard
22
23
24class Dimension:
25 """
26 Specified dimension (width/height) of a user control or window.
27
28 The layout engine tries to honor the preferred size. If that is not
29 possible, because the terminal is larger or smaller, it tries to keep in
30 between min and max.
31
32 :param min: Minimum size.
33 :param max: Maximum size.
34 :param weight: For a VSplit/HSplit, the actual size will be determined
35 by taking the proportion of weights from all the children.
36 E.g. When there are two children, one with a weight of 1,
37 and the other with a weight of 2, the second will always be
38 twice as big as the first, if the min/max values allow it.
39 :param preferred: Preferred size.
40 """
41
42 def __init__(
43 self,
44 min: int | None = None,
45 max: int | None = None,
46 weight: int | None = None,
47 preferred: int | None = None,
48 ) -> None:
49 if weight is not None:
50 assert weight >= 0 # Also cannot be a float.
51
52 assert min is None or min >= 0
53 assert max is None or max >= 0
54 assert preferred is None or preferred >= 0
55
56 self.min_specified = min is not None
57 self.max_specified = max is not None
58 self.preferred_specified = preferred is not None
59 self.weight_specified = weight is not None
60
61 if min is None:
62 min = 0 # Smallest possible value.
63 if max is None: # 0-values are allowed, so use "is None"
64 max = 1000**10 # Something huge.
65 if preferred is None:
66 preferred = min
67 if weight is None:
68 weight = 1
69
70 self.min = min
71 self.max = max
72 self.preferred = preferred
73 self.weight = weight
74
75 # Don't allow situations where max < min. (This would be a bug.)
76 if max < min:
77 raise ValueError("Invalid Dimension: max < min.")
78
79 # Make sure that the 'preferred' size is always in the min..max range.
80 if self.preferred < self.min:
81 self.preferred = self.min
82
83 if self.preferred > self.max:
84 self.preferred = self.max
85
86 @classmethod
87 def exact(cls, amount: int) -> Dimension:
88 """
89 Return a :class:`.Dimension` with an exact size. (min, max and
90 preferred set to ``amount``).
91 """
92 return cls(min=amount, max=amount, preferred=amount)
93
94 @classmethod
95 def zero(cls) -> Dimension:
96 """
97 Create a dimension that represents a zero size. (Used for 'invisible'
98 controls.)
99 """
100 return cls.exact(amount=0)
101
102 def __repr__(self) -> str:
103 fields = []
104 if self.min_specified:
105 fields.append(f"min={self.min!r}")
106 if self.max_specified:
107 fields.append(f"max={self.max!r}")
108 if self.preferred_specified:
109 fields.append(f"preferred={self.preferred!r}")
110 if self.weight_specified:
111 fields.append(f"weight={self.weight!r}")
112
113 return "Dimension({})".format(", ".join(fields))
114
115
116def sum_layout_dimensions(dimensions: list[Dimension]) -> Dimension:
117 """
118 Sum a list of :class:`.Dimension` instances.
119 """
120 min = sum(d.min for d in dimensions)
121 max = sum(d.max for d in dimensions)
122 preferred = sum(d.preferred for d in dimensions)
123
124 return Dimension(min=min, max=max, preferred=preferred)
125
126
127def max_layout_dimensions(dimensions: list[Dimension]) -> Dimension:
128 """
129 Take the maximum of a list of :class:`.Dimension` instances.
130 Used when we have a HSplit/VSplit, and we want to get the best width/height.)
131 """
132 if not len(dimensions):
133 return Dimension.zero()
134
135 # If all dimensions are size zero. Return zero.
136 # (This is important for HSplit/VSplit, to report the right values to their
137 # parent when all children are invisible.)
138 if all(d.preferred == 0 and d.max == 0 for d in dimensions):
139 return Dimension.zero()
140
141 # Ignore empty dimensions. (They should not reduce the size of others.)
142 dimensions = [d for d in dimensions if d.preferred != 0 and d.max != 0]
143
144 if dimensions:
145 # Take the highest minimum dimension.
146 min_ = max(d.min for d in dimensions)
147
148 # For the maximum, we would prefer not to go larger than then smallest
149 # 'max' value, unless other dimensions have a bigger preferred value.
150 # This seems to work best:
151 # - We don't want that a widget with a small height in a VSplit would
152 # shrink other widgets in the split.
153 # If it doesn't work well enough, then it's up to the UI designer to
154 # explicitly pass dimensions.
155 max_ = min(d.max for d in dimensions)
156 max_ = max(max_, max(d.preferred for d in dimensions))
157
158 # Make sure that min>=max. In some scenarios, when certain min..max
159 # ranges don't have any overlap, we can end up in such an impossible
160 # situation. In that case, give priority to the max value.
161 # E.g. taking (1..5) and (8..9) would return (8..5). Instead take (8..8).
162 if min_ > max_:
163 max_ = min_
164
165 preferred = max(d.preferred for d in dimensions)
166
167 return Dimension(min=min_, max=max_, preferred=preferred)
168 else:
169 return Dimension()
170
171
172# Anything that can be converted to a dimension.
173AnyDimension = Union[
174 None, # None is a valid dimension that will fit anything.
175 int,
176 Dimension,
177 # Callable[[], 'AnyDimension'] # Recursive definition not supported by mypy.
178 Callable[[], Any],
179]
180
181
182def to_dimension(value: AnyDimension) -> Dimension:
183 """
184 Turn the given object into a `Dimension` object.
185 """
186 if value is None:
187 return Dimension()
188 if isinstance(value, int):
189 return Dimension.exact(value)
190 if isinstance(value, Dimension):
191 return value
192 if callable(value):
193 return to_dimension(value())
194
195 raise ValueError("Not an integer or Dimension object.")
196
197
198def is_dimension(value: object) -> TypeGuard[AnyDimension]:
199 """
200 Test whether the given value could be a valid dimension.
201 (For usage in an assertion. It's not guaranteed in case of a callable.)
202 """
203 if value is None:
204 return True
205 if callable(value):
206 return True # Assume it's a callable that doesn't take arguments.
207 if isinstance(value, (int, Dimension)):
208 return True
209 return False
210
211
212# Common alias.
213D = Dimension
214
215# For backward-compatibility.
216LayoutDimension = Dimension