Skip to content

Commit 4a27e48

Browse files
authored
Merge branch 'main' into fix-option-list-fix-size-when-options-removed
2 parents 57caaba + e6c70b2 commit 4a27e48

File tree

8 files changed

+322
-34
lines changed

8 files changed

+322
-34
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Fixed incorrect auto height in Collapsible https://github.com/Textualize/textual/pull/5703
1414
- Fixed issue with keymaps and single-letter keys https://github.com/Textualize/textual/pull/5726
1515
- Fixed `OptionList` size after removing or clearing options https://github.com/Textualize/textual/issues/5728
16+
- Fixed footer / key panel not updating when keymaps are applied https://github.com/Textualize/textual/pull/5724
17+
- Fixed alignment not being applied when there are min and max limits on dimensions https://github.com/Textualize/textual/pull/5732
1618

1719
### Changed
1820

@@ -121,7 +123,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
121123

122124
- Fixed OptionList.add_options exhausting iterator https://github.com/Textualize/textual/pull/5540
123125
- Fixed screen not refreshing after pop https://github.com/Textualize/textual/pull/5543
124-
- Fixed footer / key panel not updating when keymaps are applied https://github.com/Textualize/textual/pull/5724
125126

126127
## [2.0.1] - 2025-02-16
127128

src/textual/_arrange.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def arrange(
102102
container_width, container_height = dock_region.size
103103
placement_offset += styles._align_size(
104104
bounding_region.size,
105-
Size(
105+
widget._extrema.apply_dimensions(
106106
0 if styles.is_auto_width else container_width,
107107
0 if styles.is_auto_height else container_height,
108108
),

src/textual/_extrema.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from fractions import Fraction
4+
from typing import NamedTuple
5+
6+
from textual.geometry import Size
7+
8+
9+
class Extrema(NamedTuple):
10+
"""Specifies minimum and maximum dimensions."""
11+
12+
min_width: Fraction | None = None
13+
max_width: Fraction | None = None
14+
min_height: Fraction | None = None
15+
max_height: Fraction | None = None
16+
17+
def apply_width(self, width: Fraction) -> Fraction:
18+
"""Apply width extrema.
19+
20+
Args:
21+
width: Width value.
22+
23+
Returns:
24+
Width, clamped between minimum and maximum.
25+
26+
"""
27+
min_width, max_width = self[:2]
28+
if min_width is not None:
29+
width = max(width, min_width)
30+
if max_width is not None:
31+
width = min(width, max_width)
32+
return width
33+
34+
def apply_height(self, height: Fraction) -> Fraction:
35+
"""Apply height extrema.
36+
37+
Args:
38+
height: Height value.
39+
40+
Returns:
41+
Height, clamped between minimum and maximum.
42+
43+
"""
44+
min_height, max_height = self[2:]
45+
if min_height is not None:
46+
height = max(height, min_height)
47+
if max_height is not None:
48+
height = min(height, max_height)
49+
return height
50+
51+
def apply_dimensions(self, width: int, height: int) -> Size:
52+
"""Apply extrema to integer dimensions.
53+
54+
Args:
55+
width: Integer width.
56+
height: Integer height.
57+
58+
Returns:
59+
Size with extrema applied.
60+
"""
61+
return Size(
62+
int(self.apply_width(Fraction(width))),
63+
int(self.apply_height(Fraction(height))),
64+
)

src/textual/canvas.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
import sys
1112
from array import array
1213
from collections import defaultdict
1314
from dataclasses import dataclass
@@ -157,7 +158,10 @@ def __init__(self, width: int, height: int) -> None:
157158
self._width = width
158159
self._height = height
159160
blank_line = " " * width
160-
self.lines: list[array[str]] = [array("u", blank_line) for _ in range(height)]
161+
array_type_code = "w" if sys.version_info >= (3, 13) else "u"
162+
self.lines: list[array[str]] = [
163+
array(array_type_code, blank_line) for _ in range(height)
164+
]
161165
self.box: list[defaultdict[int, Quad]] = [
162166
defaultdict(lambda: (0, 0, 0, 0)) for _ in range(height)
163167
]

src/textual/css/styles.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,9 +1162,9 @@ def append_declaration(name: str, value: str) -> None:
11621162
if "min_height" in rules:
11631163
append_declaration("min-height", str(self.min_height))
11641164
if "max_width" in rules:
1165-
append_declaration("max-width", str(self.min_width))
1165+
append_declaration("max-width", str(self.max_width))
11661166
if "max_height" in rules:
1167-
append_declaration("max-height", str(self.min_height))
1167+
append_declaration("max-height", str(self.max_height))
11681168
if "transitions" in rules:
11691169
append_declaration(
11701170
"transition",

src/textual/widget.py

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from textual._debug import get_caller_file_and_line
5555
from textual._dispatch_key import dispatch_key
5656
from textual._easing import DEFAULT_SCROLL_EASING
57+
from textual._extrema import Extrema
5758
from textual._styles_cache import StylesCache
5859
from textual._types import AnimationLevel
5960
from textual.actions import SkipAction
@@ -504,6 +505,8 @@ def __init__(
504505
"""Time of last scroll."""
505506
self._user_scroll_interrupt: bool = False
506507
"""Has the user interrupted a scroll to end?"""
508+
self._extrema = Extrema()
509+
"""Optional minimum and maximum values for width and height."""
507510

508511
@property
509512
def is_mounted(self) -> bool:
@@ -1528,12 +1531,16 @@ def _get_box_model(
15281531
# Container minus padding and border
15291532
content_container = container - gutter.totals
15301533

1534+
extrema = self._extrema = self._resolve_extrema(
1535+
container, viewport, width_fraction, height_fraction
1536+
)
1537+
min_width, max_width, min_height, max_height = extrema
1538+
15311539
if styles.width is None:
15321540
# No width specified, fill available space
15331541
content_width = Fraction(content_container.width - margin.width)
15341542
elif is_auto_width:
15351543
# When width is auto, we want enough space to always fit the content
1536-
15371544
content_width = Fraction(
15381545
self.get_content_width(content_container - margin.totals, viewport)
15391546
)
@@ -1555,28 +1562,17 @@ def _get_box_model(
15551562
if is_border_box:
15561563
content_width -= gutter.width
15571564

1558-
if styles.min_width is not None:
1565+
if min_width is not None:
15591566
# Restrict to minimum width, if set
1560-
min_width = styles.min_width.resolve(
1561-
container - margin.totals, viewport, width_fraction
1562-
)
1563-
if is_border_box:
1564-
min_width -= gutter.width
15651567
content_width = max(content_width, min_width, Fraction(0))
15661568

1567-
if styles.max_width is not None and not (
1569+
if max_width is not None and not (
15681570
container.width == 0
15691571
and not styles.max_width.is_cells
15701572
and self._parent is not None
15711573
and self._parent.styles.is_auto_width
15721574
):
15731575
# Restrict to maximum width, if set
1574-
max_width = styles.max_width.resolve(
1575-
container - margin.totals, viewport, width_fraction
1576-
)
1577-
if is_border_box:
1578-
max_width -= gutter.width
1579-
15801576
content_width = min(content_width, max_width)
15811577

15821578
content_width = max(Fraction(0), content_width)
@@ -1614,28 +1610,16 @@ def _get_box_model(
16141610
if is_border_box:
16151611
content_height -= gutter.height
16161612

1617-
if styles.min_height is not None:
1613+
if min_height is not None:
16181614
# Restrict to minimum height, if set
1619-
min_height = styles.min_height.resolve(
1620-
container - margin.totals, viewport, height_fraction
1621-
)
1622-
if is_border_box:
1623-
min_height -= gutter.height
16241615
content_height = max(content_height, min_height, Fraction(0))
16251616

1626-
if styles.max_height is not None and not (
1617+
if max_height is not None and not (
16271618
container.height == 0
16281619
and not styles.max_height.is_cells
16291620
and self._parent is not None
16301621
and self._parent.styles.is_auto_height
16311622
):
1632-
# Restrict maximum height, if set
1633-
max_height = styles.max_height.resolve(
1634-
container - margin.totals, viewport, height_fraction
1635-
)
1636-
1637-
if is_border_box:
1638-
max_height -= gutter.height
16391623
content_height = min(content_height, max_height)
16401624

16411625
content_height = max(Fraction(0), content_height)
@@ -2202,6 +2186,63 @@ def is_on_screen(self) -> bool:
22022186
return False
22032187
return True
22042188

2189+
def _resolve_extrema(
2190+
self,
2191+
container: Size,
2192+
viewport: Size,
2193+
width_fraction: Fraction,
2194+
height_fraction: Fraction,
2195+
) -> Extrema:
2196+
"""Resolve minimum and maximum values for width and height.
2197+
2198+
Args:
2199+
container: Size of outer widget.
2200+
viewport: Viewport size.
2201+
width_fraction: Size of 1fr width.
2202+
height_fraction: Size of 1fr height.
2203+
2204+
Returns:
2205+
Extrema object.
2206+
"""
2207+
2208+
min_width: Fraction | None = None
2209+
max_width: Fraction | None = None
2210+
min_height: Fraction | None = None
2211+
max_height: Fraction | None = None
2212+
2213+
styles = self.styles
2214+
container -= styles.margin.totals
2215+
if styles.box_sizing == "border-box":
2216+
gutter_width, gutter_height = styles.gutter.totals
2217+
else:
2218+
gutter_width = gutter_height = 0
2219+
2220+
if styles.min_width is not None:
2221+
min_width = (
2222+
styles.min_width.resolve(container, viewport, width_fraction)
2223+
- gutter_width
2224+
)
2225+
2226+
if styles.max_width is not None:
2227+
max_width = (
2228+
styles.max_width.resolve(container, viewport, width_fraction)
2229+
- gutter_width
2230+
)
2231+
if styles.min_height is not None:
2232+
min_height = (
2233+
styles.min_height.resolve(container, viewport, height_fraction)
2234+
- gutter_height
2235+
)
2236+
2237+
if styles.max_height is not None:
2238+
max_height = (
2239+
styles.max_height.resolve(container, viewport, height_fraction)
2240+
- gutter_height
2241+
)
2242+
2243+
extrema = Extrema(min_width, max_width, min_height, max_height)
2244+
return extrema
2245+
22052246
def animate(
22062247
self,
22072248
attribute: str,

0 commit comments

Comments
 (0)